Friday, 2 January 2015

Specing the API


When you cover your API with specs the first rule is to cover everything. 
Since the API can be called by third parties you need to be sure what is happening in every case. You must be able to reproduce every scenario any time and you must have the same results. :) Does it seems scientific? Well... Indeed it is.

Let me show you some examples:
-You might need to check that only the calls with developer key can access the API. In the opposite case the response is 401, it is still a JSON and maybe an audit log is created.
-You might need to check that the user is authenticated. If not then the response is 401, it is still a JSON and again an audit log is created.
-If some of the parameters are missing or wrong then the response is 400, it is still a JSON and an audit log is created by logging all the incoming parameters.
-If everything is alright then the response is 200/201, the response is a JSON and the entities are massaged as needed by that specific case.

You also need to keep your specs DRY
This will reduce maintenance effort of the test code and it will keep your specs more readable and you will have easy time to add new features and you will feel more happy and in control.

As you can see the above steps like checking the response code, checking if the response format is JSON, checking that the audit log is created are repetitive tasks and can be DRY-ed with rspecs shared examples. The only specific thing which changes from one api endpoint to another is "the entities are massaged as needed by that specific case".

RSpec.shared_examples "returning 401" do
  specify "returns 401" do
    api_call params, developer_header
    expect(response.status).to eq(401)
  end
end
RSpec.shared_examples "being JSON" do
  specify 'returns JSON' do
    api_call params, developer_header
    expect { JSON.parse(response.body) }.not_to raise_error
  end
end
...

Then in your specs you can use these shared examples in particular cases:

context '/API/learn_by_playing/' do
  def api_call *params
    get "/api/learn_by_playing", *params
  end
  let(:api_key) { create :apikey }
  let(:developer_header) { {'Authorization' => api_key.token} }
  context 'GET' do
    let(:required_params) do
      {
        :first_name => 'Botond',
        :last_name => "Orban"
      }
    end
    let(:params) { required_params }

    it_behaves_like 'restricted for developers'

    context 'wrong parameters' do
      required_params.keys.each do |s|
        context 'when the #{s} parameter is blank' do
          let(:params) { required_params.merge({s => ''}) }
          it_behaves_like 'returning 400'
          it_behaves_like 'being JSON'
          it_behaves_like 'creating an audit log'
        end
        context 'when the #{s} parameter is missing' do
          let(:params) { required_params.except(s) }
          it_behaves_like 'returning 400'
          it_behaves_like 'being JSON'
          it_behaves_like 'creating an audit log'
        end
      end
    end
    context 'valid params' do
      specify '...whatever you need to really assert for in this particular case...' do
        api_call params, developer_header
        expect ...
      end
    end
  end
end

As you can see from the above example using Rspec.shared examples a lot of repetitive lines from your Rspec can be thrown out.

On one of my projects I managed to reduce the specs size by 20% and raise the visibility a lot :) I can't measure and therefore I can't express in numbers what "a lot" means but everybody was satisfied  and pleased with the result.

Professional DRY hacking ;)