Your test cases should fail

il3ven

il3ven

Posted on January 22, 2022

Your test cases should fail

Writing tests are not enough. If the tests don't fail when a bug is introduced then what's the point of writing tests. Your test should fail when the application doesn't meet the requirements. In this article, we look at an example, then improve its test and later end with a few guidelines for writing tests.

Example

Suppose we are making an API that returns information about a user.

API

# GET /users/defunkt
$ curl https://api.github.com/users/defunkt

> {
>   "login": "defunkt",
>   "id": 2,
>   "node_id": "MDQ6VXNlcjI=",
>   "avatar_url": "https://avatars.githubusercontent.com/u/2?v=4",
>   "gravatar_id": "",
>   "url": "https://api.github.com/users/defunkt",
>   "html_url": "https://github.com/defunkt",
>   ...
> }
Enter fullscreen mode Exit fullscreen mode

Test for the API

The following code sends a request to /user/defunkt. The test passes if the status code of the response is 200 else it fails.

test('if /user/:id API works', () => {
  const response = request(app).get('/user/defunkt');

  expect(response.statusCode).toBe(200);
})
Enter fullscreen mode Exit fullscreen mode

What's wrong with the above test?

Imagine that while developing, someone makes a mistake and node_id is now missing from the API's response. Our test will still pass because the response status code is still 200.

Modifying the test to be more robust

Here, in addition to the status code, we check if the response has the desired properties.

test('if /user/:id API works', () => {
  const response = request(app).get('/user/defunkt');

  expect(response.statusCode).toBe(200);

  expect(response.login).toBeDefined();
  expect(response.id).toBeDefined();
  expect(response.node_id).toBeDefined();
  expect(response.avatar_url).toBeDefined();
  ...
})
Enter fullscreen mode Exit fullscreen mode

Are we done?

We have made progress. Our test case fail when a property isn't present, but we can do better. If our API returns avatar_url: '' our app will crash but the test will still pass.

Modifying the test case to make them more robust

Here we check if avatar_url is a string and not an empty string.

test('if /user/:id API works', () => {
  const response = request(app).get('/user/defunkt');

  expect(response.statusCode).toBe(200);

  expect(response.login).toBeDefined();
  expect(response.id).toBeDefined();
  expect(response.node_id).toBeDefined();
  expect(response.avatar_url).toBeDefined();

  expect(typeof response.avatar_url).toBe('string');
  expect(response.avatar_url.length).toBeGreaterThan(0);
  ...
})
Enter fullscreen mode Exit fullscreen mode

Guidelines for writing better tests

Code coverage is a necessary metric but not enough

In the subsequent iterations of our tests we didn't increase the code coverage. We only added more expect statements. Thus, a higher code coverage doesn't mean better tests.

When a bug is found, write a test for it

Great tests can't always be written in the first iteration, but over time they can be improved. Writing a test case for a bug ensures that the bug doesn't resurface.

Test driven development (TDD)

By the nature of TDD, it ensures that all your requirements are written down as tests. If the code fails to meet any requirement, the tests will fail.

Use the philosophy that your test should fail

In our first and second iterations, the test case did not fail in certain cases. Add a test for everything that can go wrong. In the above example, a property could be undefined, so we added an expect for it.

💖 💪 🙅 🚩
il3ven
il3ven

Posted on January 22, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Your test cases should fail
testing Your test cases should fail

January 22, 2022