Unit tests: Run in isolation. Run fast. Run deterministically.
January 3, 2022
Blog > Development, Quality, TDD, Unit Testing | Valentina Cupać
Unit tests: Run in isolation. Run fast. Run deterministically.
- Running in isolation means the tests should be independent of each other, and independent of the external world. This means we can run them in parallel, in any sequence, without affecting each other. It also means that anything in the external world will not be able to affect the tests.
- Running fast implies that the tests will be run in-memory. This means we can get really fast feedback, and we can run the tests throughout our development without slowing us down.
- Running deterministically means that we should be able to fully control the inputs in order to get deterministic outputs. This means the tests will always pass, or always fail, deterministically. This means we can safely use them as regression tests.
FUNDAMENTAL CONCEPTS
Conceptually, it means we must have the following barrier between:
- The internal world (our system under test) – here we want to have full control
- The external world (external systems) – which we may not be able to fully control
PRACTICAL GUIDELINES
For code to be unit-testable, it must adhere to the following:
– Should NOT directly reference I/IO operations, so don’t talk to the file system, database, networking. WHY? If we use these shared resources, then our tests can’t run in isolation since they interfere with each other. This means we also get fragile tests that sometimes fail and sometimes pass depending on the state of the shared resources. Furthermore, I/O operations are slow, hence our tests wouldn’t be able to run fast.
– Should NOT directly reference the environment, e.g. should avoid directly referencing default regional settings, default character encoding, default newline character, etc. WHY? If we depend on the environment, then it could mean that our tests run on one machine, but fail on another, due to different environment settings. It could also cause differences when running on different operating systems. This means we don’t have true isolation. Tests should be independent of the environment, and should not require any special environment settings, or any other configuration settings.
– Should NOT directly reference any global mutable state, e.g. should not reference any mutable singleton. WHY? Referencing global mutable state means that the tests can no longer run in parallel in isolation because one test affects the other tests due to the shared global state.
– Should NOT directly reference randomness, e.g. system clock, random number generator, sequence generator. WHY? When we directly reference sources of randomness, then our tests are not deterministic
In order to achieve the above, it means you need to use dependency injection.
This enables you to create a Test Double (Mock / Stub / Fake) for any such external dependencies in unit tests.
References: Michael Feathers https://buff.ly/2LFNuVH, Martin Fowler https://buff.ly/2RydulY