Previously: This article continues fromDefining the API using code-first approach.

Define your testing strategy

It may be tempting to overlook the importance of a good testing strategy whenstarting a new project. Initially, as the project is small and you mostly keepadding new code, even a badly-written test suite seems to work well. However, asthe project grows and matures, inefficiencies in the test suite can severelyslow down progress.

A good test suite has the following properties:

  • Speed: The test suite should complete quickly. This encourages shortred-green-refactor cycle, which makes it easier to spot problems, becausethere have been few changes made since the last test run that passed. It alsoshortens deployment times, making it easy to frequently ship small changes,reducing the risk of major breakages.
  • Reliability: The test suite should be reliable. No developer enjoysdebugging a failing test only to find out it was poorly written and failuresare not related to any problem in the tested code. Flaky tests reduce thetrust in your tests, up to point where you learn to ignore these failures,which will eventually lead to a situation when a test failed legitimatelybecause of a bug in the application, but you did not notice.
  • Isolation of failures: The test suite should make it easy to isolate thesource of test failures. To fix a failing test, developers need to find thespecific place that does not work as expected. When the project containsthousands of lines and the test failure can be caused by any part of thesystem, then finding the bug is very difficult, time consuming anddemotivating.
  • Resilience: The test implementation should be robust and resilient tochanges in the tested code. As the project grows and matures, you may need tochange existing behavior. With a brittle test suite, each change may breakdozens of tests, for example when you have many end-to-end/UI tests that relyon specific UI layout. This makes change prohibitively expensive, up to apoint where you may start questioning the value of such test suite.References:

  • Test Pyramid by MartinFowler

  • The testing pyramidby Jonathan Rasmusson
  • Just say no to more end-to-end tests
  • 100,000 e2e selenium tests? Sounds like a nightmare!
  • Growing Object-Oriented Software Guided by Tests

How to build a great test suite

To create a great test suite, think smaller and favor fast, focused unit-testsover slow application-wide end-to-end tests.

Say you are implementing the “search” endpoint of the Product resource describedearlier. You might write the following tests:

  • One “acceptance test”, where you start the application, make an HTTP requestto search for a given product name, and verify that expected products werereturned. This verifies that all parts of the application are correctlywired together.

  • Few “integration tests” where you invoke ProductController API fromJavaScript/TypeScript, talk to a real database, and verify that the queriesbuilt by the controller work as expected when executed by the databaseserver.

  • Many “unit tests” where you test ProductController in isolation and verifythat the controller handles all different situations, including error pathsand edge cases.

Testing workflow

Here is what your testing workflow might look like:

  • Write an acceptance test demonstrating the new feature you are going tobuild. Watch the test fail with a helpful error message. Use this new testas a reminder of what is the scope of your current work. When the new testspasses then you are done.

  • Think about the different ways how the new feature can be used and pick onethat’s most easy to implement. Consider error scenarios and edge cases thatyou need to handle too. In the example above, where you want to search forproducts by name, you may start with the case when no product is found.

  • Write a unit-test for this case and watch it fail with an expected (andhelpful) error message. This is the “red” step in Test Driven Development(TDD).

  • Write a minimal implementation need to make your tests pass. Building up onthe example above, let your search method return an empty array. This is the“green” step in TDD.

  • Review the code you have written so far, and refactor as needed to clean upthe design. Don’t forget to keep your test code clean too! This is the“refactor” step in TDD.

  • Repeat the steps 2-5 until your acceptance test starts passing.

When writing new unit tests, watch out for situations where your tests areasserting on how the tested objects interacted with the mocked dependencies,while making implicit assumptions about what is the correct usage of thedependencies. This may indicate that you should add an integration test inaddition to a unit test.

For example, when writing a unit test to verify that the search endpoint isbuilding a correct database query, you would usually assert that the controllerinvoked the model repository method with an expected query. While this gives usconfidence about the way the controller is building queries, it does not tell uswhether such queries will actually work when they are executed by the databaseserver. An integration test is needed here.

To summarize:

  • Pay attention to your test code. It’s as important as the “real” code you areshipping to production.
  • Prefer fast and focused unit tests over slow app-wide end-to-end tests.
  • Watch out for integration points that are not covered by unit-tests and addintegration tests to verify your units work well together.See Testing Your Application for a referencemanual on automated tests.