On testing

Augusts Bautra - Sep 25 '23 - - Dev Community

Why test?

  • Improves development quality and speed - needed state is usually faster and easier to set up and manipulate in test environment.
  • Establishes confidence that things work as needed and will continue to do so.
  • Serves as documentation.
  • Drives better design - if it's hard to test, it's poorly designed.

What to test?

Follow J.B. Rainsberger's idea of "isolated tests" - test pieces of source code in a single file, usually public methods.

Do contract testing - stub out calls to other class methods, but assert that the calls happen with correct parameters and that code under test correctly handles the mocked return value. This way code close to input/output (for example controllers) need not run through the whole app stack.

# sample action
def update
  record = find_user!
  return render_record_missing_error unless record

  result = SomeService.call(
    user: record,
    company: current_company,
    options: safe_params.to_hash,
  )

  if result.success?
    render(json: result, status: :ok)   
  else
    render(json: result, status: :unprocessable_entity)
  end 
end
Enter fullscreen mode Exit fullscreen mode

Focusing on only testing the code written in the file worked on tells us that SomeService.call should be treated as a black box and stubbed out. We do not care what side-effects it produces, only that code under test can use its return value properly.
Furthermore, we have three ways out of this method:

  1. early return due to missing user and two proper results
  2. a success
  3. a failure

Covering these scenarios should reach 100% coverage without excessive specs.

# sample good spec
describe SomeController, "update" do
  context "when user can not be found" do
    it { responds_with_record_missing }
  end

  context "when SomeService call returns a success outcome" do
    before do
      allow(SomeService).to receive(:call).and_return(success_outcome)
    end

    it do
      responds_with_success_json

      expect(SomeService).to have_received(:call).with(
        user: user,
        company: company,
        options: {some: param},
      ).once
    end
  end

  context "when SomeService call returns a failure outcome" do
    before do
      allow(SomeService).to receive(:call).and_return(failure_outcome)
    end

    it { responds_with_failure_json }
  end
end
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .