Writing software tests

Mark Boyd
3 min readNov 3, 2021

Key recommendations

  • When possible, write functions (or React components) that are as “pure” as possible (functions that produce output from input with no side effects), because they are easier to test
  • Use mocking to isolate unit tests, but when the mocking for the test feels more complicated than the code being tested, you may want to re-examine your code structure or explore alternative testing strategies
  • At minimum, use a TDD style approach when fixing bugs. Write the failing test, apply the prospective bug fix, and ensure that the tests pass
  • Include code coverage in your CI, but don’t go for 100% coverage and don’t assume it means your code is well tested

Benefits of writing tests

  • Tests help to prevent regressions
  • Tests serve as living documentation of what your code is expected to do and can be really helpful for those onboarding to your project or users of your code
  • Writing tests forces you as a developer to understand what the expected behavior of your code is and may help identify potential opportunities for refactoring

Unit tests

  • Unit tests evaluate the behavior of a single unit of code. The most obvious example of a unit test is code that tests the behavior of a single function, but that unit could be a function, a class, a React component, and more.
  • Examples: React component tests (Enzyme), JUnit (Java), unittest (Python)

Integration tests

  • Integration tests evaluate the behavior and interaction of multiple units of code or software layers. The point of integration tests is to ensure that your software behaves as expected when multiple code units or layers are actually interacting with each other.
  • Integration tests are critical because it is completely possible (and common) to have each individual unit of code passing tests but for those code units to not behave as expected when integrated together
  • Integration tests do not have to test the integration of all of your software components. You can have an integration test that only tests the integration between two code units or software layers.
  • Examples: Cypress browser tests, Behavior-driven tests, Jasmine (Javascript)

Using mocking in your tests

  • Mocking provides isolation for your tests and allows you to ensure that you are testing the behavior of your code and not dependencies that are used by your code
  • Simulate edge-case behavior (e.g. mock a dependency method to throw an unexpected error)
  • Examples: Mocking the API layer or responses for your front-end tests, mock responses from external libraries/code in unit tests
  • Key question — If this test failed, where would the fix be? If the answer is ever “not in this function” or “not in this service”, then you might consider mocking that dependency for the test, otherwise your test’s success is at least partially dependent on that external function/service

How many tests do I need?

  • Code coverage is useful to get a rough sense of how many of your code’s lines are tested, but the coverage amount says nothing about the quality of those tests. So using 100% as a target to ensure your project is “well tested” may be misleading. 70% code coverage is often mentioned as a good goal for a project, but there’s nothing magical about this particular number.
  • There are some testing methodologies that attempt to prescribe how many unit tests you should write, such as Structured Basis Testing

When to write tests

  • Classical Test Driven Development (TDD) — Write tests as you’re developing the code. The benefit of this approach is that it forces you to think up front about your code behavior and encourages you to write testable code from the beginning.
  • You can also take a TDD-lite approach when diagnosing bugs: write a test that reproduces the bug, apply the expected fix, then ensure that the test passes. This approach gives you confidence that your changes have actually resolved the bug.

--

--