How we documented our API using unit testing

January 23, 2018 | Christophe Yammouni | 4-minute read

At Drivy, we have an internal API to communicate with our native apps available on both iOS and Android. One of the main pain point we experienced is documentation.

As our product is constantly evolving, we need to have up-to-date documentation in order to help both mobile and backend developers stay aware of what each endpoint is expecting and returning.

Static documentation is known to be hard to maintain, it’s indeed easy to forget to update it from time to time. What is more, you quickly end up with differences between documentation and actual behaviour.

The solution we are going to see is something between a static and a live documentation.

Backend stack

Our backend is written in Rails and we use Rspec as part of our test suite. For our API, we use active_model_serializers to handle the view component of the MVC pattern. Here, views are called serializers. We tried other options like RABL, but felt that active_model_serializers was the best choice. For example, its DSL is inspired from Rails, so a Ruby developer does not need special training to learn how to use it, it’s also more simple to do unit testing.

We tend to have a serializer per action and some nested nodes are generated by shared ones.

Our solution

Here’s an example of one of our serializer spec.

let(:expected_json) do
  {
    "id": user.id,
    "first_name": "Ben",
    "last_name": "Driver",
    "email": "ben@driver.com",
    "phone_number_national": "0612345678",
    "phone_country": "FR",
    "address_line1": "28 rue des paquerettes",
    "address_line2": "appt B",
    "city": "Suresnes",
    "postal_code": "92150",
    "country": "FR",
    "about_me": "I love cars",
    "license_number": "1234567890",
    "license_first_issue_date": "2001-01-01",
    "license_country": "FR",
    "birth_date": "1981-01-01",
    "birth_place": "macon",
    "created_at": "2016-01-01T11:00:00Z",
    "avatar": {
      "thumb_url": "https://drivy-test.imgix.net/uploads/originals/ed3585a06f3ad9a1c945456953cb9ed7.jpeg?auto=format%2Ccompress&crop=faces&dpr=2.0&fit=crop&fm=png&h=100&mask=https%3A%2F%2Fdrivy-prod-static.s3.amazonaws.com%2Fimgix%2Favatar_mask_circle.png&w=100"
    },
    "license_release_date": "2001-01-01",
    "stats": {
      "ratings_average": ratings_average,
      "ratings_count": 2,
      "owner_ended_rentals_count": 1,
      "driver_ended_rentals_count": 1
    }
  }
end

it_behaves_like "a serializer"
shared_examples_for "a serializer" do
  it "matches the expected output" do
    expect(JSON.pretty_generate(generated_json)).to eq JSON.pretty_generate(expected_json)
  end
end

Under the hood, we generate the JSON output from the serializer, and compare it to our expected_json variable. If any difference is spotted, the spec will fail.

What’s really interesting here is that this is an actual test. This means that any change to the serializer will break the spec, preventing a release to production and therefore forcing developers to actually update the test.

So what we see in this file will always be synchronised with the production generated content.

From a quick look, you can learn attribute names, possible values/types, and if some changes are tied to a specific app version, you can have different context, exposing those behaviors.

Conclusion

We’ve been using this solution for more than a year, with new developers joining the team, and it still fits our needs. Contexts and expected outputs are easy to read by a non-Ruby developer.

Of course, we have room for improvements, for example, it’s missing description of attributes, endpoints path, etc. But it’s a win-win solution, because we have all benefits of unit testing, and it acts as a documentation.

With some additional work, we could easily generate a standalone documentation on a regular basis regrouping all serializers output in a single page.

We’ve only been seeing a standalone piece of documentation for what a client can expect to receive from requesting our API. But what about inputs? What does a client need to send as parameters to properly receive those answers. In a future article, we’ll see how we managed to document request inputs.

👍  Like this post? Join Drivy's engineering team! View openings