Why I do love test-driven development

31 July 2021

Today I want to speak about development approach I prefer the most. It is awesome, and I would recommend it to everyone who
wants to improve his coding and architectural skills.
I will absolutely not say that TDD(test-first) is better than TLD(test-last) because there is no point in that.
At the best it will end up with "if you do it right you can't do it wrong". I will write about that someday in different post.

What is TDD

TDD or test-driven development is an approach to write code after the test rather than test after the code.
It does not mean that developer shall write a complete test before starting writing a code.
The idea is to start writing a code by questioning "what am I doing and why?".
Those two questions by themselves significantly improve code and domain quality.

How it works

We will start with the task. With a real example. Let it be this blog itself.
Assume we have an app and api (frontend and backend). Let's say we have a following task:

We already have functionality to create posts. Now we need a page with the list of all posts.
It must have a simple pagination to list forward or backward(if there are more posts than we see on a page).

By the time we start working on this task, we are already using TDD even if we have never heard about that approach.
It's the natural way to make solutions.
Our reasoning will be something as follows:

As the backend developer

  1. An application will send us an HTTP request that requires list of posts.
  2. We respond with a list of posts in some data representation in exact format(json, xml, etc).
  3. It also requires pagination, so it sends limit, offset and expects us to properly handle that and return some details to figure out if there is next/previous page.
  4. We have to create new route to handle that request.
  5. Let's name that route as GET /api/posts.
  6. Let's create controller PostListController to handle that route request.
  7. The controller receives request and returns json response with a list of posts representation.
  8. As it is the post list, it is obvious that app is not going to show each post content.
  9. So we will return only the following data: slug, publishedAt, title, preview.
  10. We also need pagination, so we have to add it in response too.
  11. That means that response will look as follows:
{
  "items": [
    {
      "slug": "some-test-post",
      "title": "Some test post",
      "publishedAt": "2021-07-31T19:20:09+00:00",
      "preview": "This is the preview of the test post."
    },
    ...
  ],
  "pagination": {
    "limit": 10,
    "offset": 30,
    "total": 54
  }
}
  1. We need to create some service to retrieve those posts.
  2. That service will have signature like PostService.getPosts(limit, offset): PaginatedCollection<Post> or two services respectively PostService.getPosts(limit, offset): Collection<Post> and PostService.countPosts(): int.
  3. The service(s) will query the database and return post collection and total amount of posts in a database.
  4. After that we will render that data in representation layer and return in response.
  5. Let the representation layer be PostView.createView(post): array and will iterate through collection with it.
  6. Pagination is also on a representation layer, so PaginationView.createView(pagination) where pagination contains limit, offset, total.
  7. We encode all data in json.
  8. We return the response.
  9. Done.

As the frontend developer

I have very poor understanding of frontend because the most of my experience is backend-based. Feel free to mention my mistakes.

  1. We have some post list that needs to be presented on a page with a route (lets say /posts).
  2. That representation contains title, preview, publication date and link to the post itself.
  3. That means that we must get a collection of objects with that minimum data.
  4. We get it from the backend as it is mentioned in api GET /api/posts.
  5. We take the data and draw it in components on a page.
  6. On a page bottom there must be a left arrow if we are not at the first page.
  7. We can figure it out with offset. If an offset is higher than zero, then we are not on the first page, and we shall render arrow left in pagination.
  8. There must be a right arrow too if there is a next page. It can be calculated by offset, limit and total. If offset+limit is less than total, then there is a next page.
  9. If a user press on those arrows we must rerender page from step 4 by adding to request query param ?offset=(currentOffset ± limit)
  10. Done.

This is it. Now, if we watch closely, we will see that most steps can be started with test.
In fact, I wrote all those steps using TDD approach.
How I did that step-by-step.

  1. Created PostListControllerTest that sends GET request on /api/posts uri, and asserts that it responds 200 status code.
  2. Test fails. Created PostListController::__invoke, created route /api/posts and attached them to each other. Test passes.
  3. Added new assertion to PostListcontrollerTest that checks if the response body matches
{
  "items": [],
  "pagination": {
    "limit": 10,
    "offset": 0,
    "total": 0
  }
}

. 4. Test fails. Created valid "empty" response. Test passes. 5. Created some posts (fixtures) in database in PostListControllerTest. Changed assertion that checked response content to an appropriate data that we created in posts fixtures.
6. Test fails. Created some service to retrieve posts from the database and count them all (see step 13 in reasoning). 7. Created representation for each post that is returned from the service. Iterate through it. Put result in response. Test passes. 8. Added limit and offset query parameters to request in PostListControllerTest and changed assertions to expected result. 9. Test fails. Added limit and offset passing from PostListController to PostService. 10. Changed PostService to handle limit and offset. Test passes.

Done. We have a working code, covered with tests.

So, what is the beautiful part of all this?

If someone asks me why do I love this, I'd answer: For the fact that we do nothing more than what we needed to do.
We have a code that fully fulfils the task at its current condition, and we have tests for that code that we should write anyway.
The less code we write, the better it is.
For beginners and middle-tier engineers it is also a good start to write clean code more effectively. TLD helps with that too.
The code that is hard to test is most likely a bad code.
The thing is, because TLD is involved *afterwards, developers tend to write bad tests for a code that is hard to test.

As I mentioned before, I am not against TLD. In fact, I use all tools depending on a situation. TLD is not an exception.
Just sharing my experience. Let your code be clean, and the tests awesome :)

Don't take everything plain: we have to challenge and prove the information we face.

Here is what really helps me to do it.