Why You Should Practice Test-driven Development
Tags: testing, development, automation, programming
Published: 2024-01-30 17:08:40

When I was a new programmer, I was at some point introduced to the concept of automated testing. While I could see the benefit of ensuring that my code works as intended, it initially seemed like unnecessary boilerplate to write tests for code I already knew worked.  Therefore, for a long time when I was working on projects, I did not write a single test. It was simply too much work to set up a test environment and then writing tests for my code. I didn't see the point at the time.

The problem was the way I was introduced to testing. The way where testing is just checking that the code that you wrote is the code that you wrote. These tests are pointless and certainly check the wrong aspects, so it's no wonder I was discouraged by automated testing. 

When doing it right, testing is a very powerful tool that helps you in many ways when you are developing software. I didn't understand it back then, but now I am very grateful for the skill I have learned: The art of Test-driven development.

So, what is Test-driven development exactly? It is a style of programming where you write the tests before you write the actual logic. 'Now, what's the point with that?', you may ask. It is simple. When your code is designed to be testable, it typically enforces good practices like a clear separation of concerns, resulting in more modular and reusable code. The result? Clean, readable code.

Let's illustrate that with an example. Imagine you have to write a piece of code that when a user has liked some post, the number of total likes of the post should go up. However, if a user has already liked a post, the number of total likes should not go up, but stay the same. We can write a function for that:

```
async function like(postId, userId) { 
  // Get the user and post from provided IDs 
   const user = await User.getById(userId);
   const post = await Post.getById(postId);

  //Check if post has been liked by user 
  if (user.likedPosts.includes(post)) { 
    return "User has already liked this post"; 
  }

  //Increment the number of likes and save the post in the users likedPosts
   post.likes += 1;
   user.likedPosts.push(post);

  //Save the changes
   await user.save();
   await post.save();
 }

```

The function is quite straightforward. It takes in a postId and a userId, gets the post and user and checks if the user has liked the post. If the user has not already liked the post, the post will be liked and saved to the users liked posts.

Let's test this scenario: A user has liked a post, and tries to like it again. The number of likes should not go up, since the user has already liked it.

We write the following test:

```
test("should not increment the number of total likes if user has already liked", async () => { 
  let user; 
  let post;

  createNewUser().then(async (res) => {
    user = await User.getById(res.data.id);
  });
  createNewPost().then(async (res) => {
    post = await Post.getById(res.data.id);
  });

  user.likedPosts.push(post);
  await like(post.id, user.id);

  expect(await Post.getById(post.id).likes).toBe(post.likes);
})
```

Several issues exist with this test. It demands extensive setup and is challenging to read without context. Clearly, this is not an optimal test.

So how can we write a better test for our function? We can attempt to improve it, making it look better, but this doesn't address the underlying problem. It is like painting a wall full of cracks. The paint makes it look nicer, but fails to deal with the real issue: It can collapse any minute.

And that's the real power of testing. They are indicators of whether or not your code is good. If your code is difficult to test, it's probably not the tests that are the problem, but your code.

So, back to the original question: How can we write a better test for our function? The answer is... We write the test first. This is how our ideal test could look like:

```
test("should not increment the number of total likes if user has already liked", async () {
   const post = await Post.initialize();
   const user = await User.initialize(likedPosts: [post.id]);

   await user.likePost(post.id);
   expect(await Post.getLikes(post.id)).toBe(post.likes);
 })
```

It is easy to read and set up. Now all that's left is to write the actual code.

Another benefit of this approach is that our code is easier to maintain. You can always change the code underneath without changing the test itself.

Test-driven development is a very powerful technique. If you are also fascinated by this approach to testing, I encourage you to give it a try. Play with it and discover its power. 

If you wonder how to get started, I recommend this YouTube channel, where you can find a lot about testing and other interesting topics:

[https://www.youtube.com/@ContinuousDelivery](https://www.youtube.com/@ContinuousDelivery).

That's all I had for now. Thank you for reading.