Skip to content
Julian Leviston edited this page Dec 9, 2016 · 6 revisions

Hopscotch best practices

  1. Params Validation
  2. Testing

Params validation

In cases when you need to validate a list of inputs to a Workflow or Service, there are two ways to handle validation:

  1. 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.
  2. Handle validations as the first step in the workflow, so it will always call failure if the validation fails.

Validation as the first step of Workflow

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
end

Testing

  1. Testing Workflows
  2. Testing Services
  3. Testing Workflow Caller

Testing Workflows

Testing 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
end

Testing Services

Hopscotch 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
end

Testing Workflow Caller

Same 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.