Repository navigation
Best Practices
In cases when you need to validate a list of inputs to a Workflow or Service, there are two ways to handle validation:
- Give the responsibility to the controller, class or module that calls the workflow. In this case, the workflow can always assume that it will receive the correct params.
- Handle validations as the first step in the workflow, so it will always call failure if the validation fails.
Below is an example of a workflow that has a separate step for validating params. Note that the validation step can be extracted into a seperate service as well.
module AddStudentWorkflow
extend self
def call(parent_id, student_info, success: success, failure: failure)
Hopscotch::Runner.call_each(
-> { validate_params(parent_id) },
-> { AddStudentService.call(parent_id, student_info) },
success: success,
failure: failure
)
end
private
def validate_params(parent_id)
if Parent.find_by(id: parent_id)
Hopscotch::Step.success!
else
Hopscotch::Step.failure! 'invalid params'
end
end
endTesting Workflows can be done with either a unit or integration test. By unit testing the workflows, you are choosing to stub services and only test the flow of the workflow. However, a better way is to treat them like integration tests.
In the example below, a workflow test is treated like an integration test that tests success and failure of the workflow call with regard to different inputs. The advantage of doing it this way is that any failing changes introduced to this workflow or any of the corresponding services later will be picked up.
describe AddStudentWorkflow do
describe '.call' do
subject do
AddStudentWorkflow.call(parent.id, student_info)
end
context 'when param validation fails' do
let(:parent) { double id: -1 }
let(:student_info) { {} }
it do
expect(subject).to eq 'invalid params'
end
end
context 'when adding student fails' do
let(:parent) { Parent.create }
let(:student_info) {{}}
it do
expect(subject).to eq 'failed to add student'
end
end
context 'when everything is alright' do
let(:parent) { Parent.create }
let(:student_info) {{ name: 'Harry Potter', year_of_birth: 1980 }}
it do
expect(subject).to eq 'OK'
end
end
end
endHopscotch services can either handle one flow or be composed of multiple flows (combined using Hopscotch::StepComposer). Based on different scenarios, different levels of testing must be done for a service. Normally it can be implemented as a unit test. Below is an example:
describe AddStudentService do
describe '.call' do
let(:parent) { Parent.create }
subject { AddStudentService.call(parent_id, student_info) }
context 'success' do
let(:student_info) {{ name: 'Harry Potter', year_of_birth: 1980 }}
it 'calls success step' do
expect(subject.class).to eq TrueClass
end
end
context 'failure' do
let(:student_info) {}
it 'calls failure step' do
expect(subject.class).to eq Hopscotch::ErrorValue
expect(subject.value).to eq "failed to add student"
end
end
end
endSame as workflow itself, the caller of the workflow is better to be an integration test. It is also not easy to stub the success/failure calls that are passed to the workflow in order to test the flow.