Unit Test

When coding a Small Development Task, you want a way to verify that the work you are doing is what’s expected and avoid breaking the code line. This pattern describes a structure that helps you identify when a task is done and gives you assurance that your change won’t break any contracts.

Identifying Completeness and Verifying Stability

Software is complex. It can be hard to know that you implemented the work for a task correctly unless you define what to expect. Specifications are subject to interpretation, but examples can identify and clarify ambiguities.

Checking that an existing class, module, or function does what you expect and still works after you make a change is a basic part of maintaining stability in your software. It is easier to understand the source of an error in a small section of code than at a system level, but testing small-scale units can become tedious, and tests take time to execute.

Errors often show up in integration, even will each unit has passing tests. Testing in the context of a full system might seem most realistic, but setting up end-to-end tests can be slow, and it can be expensive (time and resource-wise) to create multiple sets of inputs to test. Integration tests seem more complete, but it can be challenging to cover subtle error conditions.

Some of the tools you use to isolate units for testing, such as mocks, stubs, and fakes, can give you a false sense of confidence if not written correctly. But it can be challenging to recreate a test condition for code deep in the system based on high level inputs.

Time spent writing tests for small units of functionality can seem wasteful, especially when the function seems simple. But errors at higher levels often stem from those at lower ones, and a unit test lets you identify failure points.

Writing tests means updating tests when implementation or contracts change but finding issues before integration or deployment means a lower cost to fix. Writing tests early can help ensure a more modular, testable design, as less modular systems tend to be harder to test.

What a “unit” is can be a matter of judgment, and not all team members may agree on what’s appropriate for a test.

We want to isolate integration issues from local changes and test each component’s contracts.

Test the Contract

** Develop and run Unit Tests for any changes you make. Unit tests should be quick and easy to execute in a developer workspace. Execute the tests as part of an Integration Build.**

A Unit Test test tests fine-grained elements of a component to see that they follow their contract. The tests verify the behavior of work in development and provide a mechanism to confirm that a new change hasn’t broken other code.

A good unit test (Beck and Andres, Extreme Programming Explained). is:

Tests should also be quick to run. Aim for 5 mins or fewer to run all the Unit Tests for a module. If it’s longer evaluate why.

Run Unit tests:

You can identify what to test by considering:

Two common concerns about Unit tests relate to overhead and fragility:

The isolation and self-configuring aspect of unit tests means that you may need to define mocks and stubs to ensure that you can test code. Mocks can be fragile if not implemented thoughtfully. Tools like Test Containers let you avoid mocks by running full services. This has the benefit of stability and robustness but can slow down test execution.

Coverage

When talking about unit tests the topic of test coverage often comes up. “Coverage”— the percentage of lines of code that running tests cover — is a useful metric but shouldn’t be taken to an extreme. While a low coverage number is concerning, a high number isn’t always better. Consider the value of the test and the effort to test. You may discover some code paths — in particularly those that involve external services with complex interfaces that are difficult to mock – might be easier to test using a smoke test. While there are no absolute numbers, a rule of thumb is:

If your coverage is low, and you work to add tests, keep track of how many potential bugs you uncover in your Unit Test cases. If you are finding potential errors, then the effort is worth it.

If you add tests as you develop code, the overhead of high coverage is mitigated by improved development speed.

{aside} (Story: Really Dumb Tests, and simple errors that caused big ones errors) Since adding tests isn’t always valuable you should always be asking whether it makes sense to test a function, with a bias towards avoiding testing trivial functions and methods. When on doubt, two questions you can ask are:

As a rule, the code-test-fix cycle is much faster when using unit tests than when a problem appears when running the full application. Two examples I’ve come across are:

I once worked on a VXML application where the back end server generated VXML elements in response to certain events. We discovered that the back end code would generate invalid Voice XML code causing the voice server to stop. We added a series of one line unit tests to the back end that called the “generateXML()” method with the various inputs and validated the result to the VXML Schema. While this seemed trivial, it ended up saving a lot of time.

A similar situation comes up when interacting with Database. Failures can happen in database applications for a range of issues, such as field names not being correct or when the database scheme changes and the code has not been updated or if a code path generates values that are not aligned with constraints. By writing simple tests on the database communication layer with Test Containers you can debug these kind of issues quickly. {/aside}

{aside}

Names

In a few teams I’ve seen long discussions about types of tests: Unit/Component etc…

What really matters is:

{/aside}

Cautions

Next Steps