Testing a RESTful API (part 1)

So, in my last blogpost I wrote about business rule validation in a RESTful API. Check that post out if you haven't done so already, I think it's a good one.
If you explore the repo mentioned in that blogpost, you can also see the tests I wrote for some of the functionality. It's not the most comprehensive test suite but that is not the objective, the objective of the repo is to have something concrete that helps me communicate my ideas.
Anyways, in this repo, the functionality of the API is tested exclusively by integration tests, all the assertions happen through the HTTP interface. Here's one of those test files.
@Test
void creationFailsWhenFeeNotPayed() {
var student = givenAttemptToWrite()
.body(new StudentCreationRequest("Bob", false, 11))
.basePath("student")
.post().as(StudentDto.class);
var exam = givenAttemptToWrite()
.body(new ExamCreationRequest("B"))
.basePath("exam")
.post().as(ExamDto.class);
givenAttemptToWrite()
.body(new AttemptCreationRequest(student.id(), exam.id(), ATTEMPT_DATE))
.when().post()
.then().statusCode(500);
}
Isn't this sooo easy on the eyes? And it covers so many aspects of the app, Controllers, Services, Validation and Persistence. It's all covered!
Lets have an even deeper look at the advantages of this approach to testing.
The advantage I appreciate the most is the fact that refactors are smaller in size. If the tests are too tightly coupled to the implementation details – which is usually the case with unit tests – then most refactors are likely to suffer from the Shotgun Surgery syndrome: the required change causes many small changes in other parts of the codebase. You refactor the interface of an API-internal method and 50 tests break even though the behaviour stays the same.
I think people often make the mistake of only thinking about tight coupling in the application code and forget to apply a similar way of thinking to the tests. Been there done that... This leads to codebases that are hard to extend or refactor and it has been very frustrating for me.
For emphasis, imagine this: you need to make a change that theoretically should not be changing any behaviour (maybe a refactor or something related to logging or debugging). And yet, the tests are broken because signatures of public methods used inside your API are different and those methods were directly called in many tests. Now you need to touch the tests. But if you do, can you be sure that the behaviour is still exactly the same as before? The tests HAVE changed, so there is no guarantee. But you are 100% confident. You send the pull request to your coworker. The coworker sees that you've touched over 20 test cases, they still need to do the due diligence and make a thorough code review over this big change-set.
If the tests were less coupled to the implementation the coworker could instantly discard a big set of potential flaws. Tests are green and no tests changed... Great! This looks like an easy pull request to verify.
I feel strongly about this issue because applying this to my work has improved the teamwork and the code quality a lot. And by the way, Spotify engineers have also arrived to similar conclusions way before I did. It's reassuring to know, that others have arrived to similar conclusions.
There are more advantages to this approach to testing though:
- It is much easier to achieve good code coverage.
- The tested behaviour can actually be triggered by user/client actions. With unit tests you can sometimes be testing stuff that is not reproducible through the actual API interface.
- Refactors are safer. Such tests remain unchanged if you refactor the internals of the program. A refactor is not supposed to change the behaviour, so if these tests still run, then we are probably good!
Of course there are also downsides:
- Tests could take longer to run.
- Stuff like testcontainers helps a lot but test setup could be complex.
- Spinning up the whole app to run such tests could be unrealistic if the app is huge. In this case you need to add more machinery to run slices of your app.
And let me be clear, integrations tests are not the holy grail, but they are really useful. Unit tests are also useful, at the same time, many developers write too many unit tests. Takeaway: learn about many tools and use them accordingly.
In future blogposts I will cover how to write such tests in more complex scenarios, for example with Token based authentication or with Kafka and a Schema registry.