I think everyone has heard about the pyramid of tests. It's divided into three parts: Its bottom is built of unit tests. They're the base, and you're supposed to have more of them than any other type of tests. Integration tests are in the middle, and then you should have just a few end-to-end tests on the top.

Those're the classics as defined by Martin Fowler and recommended by, among others, Google. Or are they?

Added value of tests

Another perspective on tests is to try to evaluate the added value of each test. Unit tests are cheap, fast, and easy to write — that's for sure. Generally, as you move up the pyramid, tests get harder to write, are slower to run and thus more expensive. That strongly suggests we should focus all our efforts on writing unit tests. But what about that added value?

As it turns out, when you move up the pyramid, the business value of tests significantly increases.

Is the pyramid wrong?

Well yes, but actually no. It's not inherently wrong. It's just that each level of tests plays a different role in the process of building software:

  • unit tests — help you shape your code
  • integration tests — test if features work correctly
  • e2e tests — test if the app works

Unit tests

If you want to write clean, reusable code adhering to all the best practices, you should definitely use TDD. Divide your features into units of code. Write unit tests. Write the implementation. Refactor. Repeat. You'll end up with quite a lot of unit tests, sure, but they are cheap, fast, and isolated from the rest of the codebase, meaning the maintenance cost should also be low.

The added value here is that your codebase is cleaner.

But what if you already have an app, 0 tests, and you want to start writing tests to add some value to your product? Unit tests are not very useful for testing existing apps.

First of all, they only test units of work instead of actual features. Your users don't care about units of work, they care about your app working fine. Unit tests do not even attempt to guarantee that. That's not their point at all!

I unit tested everything so I'm sure the app is working correctly!

Moreover, unit testing of an existing codebase is challenging. You end up with an impenetrable network of stubs and mocks, complex hacks, and implementation tests just because your codebase was not structured for unit tests.

Oh, and just by the way, many of your unit tests can easily be replaced by a statically and strongly typed languages such as TypeScript or Reason.

End-to-End

You should use e2e tests when you want to make sure your app works. That's a bold statement, isn't it? Right. E2e tests are expensive to write, hard to maintain, take a long time to set up and run. Despite this, you probably want to have at least one of these e2e tests. One which follows the most "happy path" of your app, just like most of your users do. That way, if something crucial breaks, you'll notice!

The value added here is that you gain some level of certainty that your app works.

Enter: Integration tests

Write integration tests if you want to make sure your features work. Add them for existing features. Write new integration tests for new features. Integrations tests all the way. Why?

Well-written integration tests don't test units of code, they don't test implementation detail. They view your features the same way as your users do! At the same time, they still only aim to test certain features and not the whole app. That makes them cheaper and faster to run than e2e tests but more reliable and useful than unit tests.

Integration tests bring the greatest value to your app. If your business asks you "which tests are most beneficial from the $$$ point of view" — you know what to answer.

Your pyramid looks more like a trophy!

You got it.

"The Testing Trophy" (source: https://twitter.com/kentcdodds/status/960723172591992832)

Summary

tl;dr:

  • use statically and strongly typed languages so that you don't have to write many small and repeatable unit tests
  • write unit tests (TDD) when you start working on anything from scratch to help you shape your code
  • don't test implementation
  • write integration tests for any new or existing features in your product to ensure they work correctly
  • prepare a few end-to-end tests with the most common scenarios your users follow

Followup: https://kentcdodds.com/blog/write-tests