> These thoughts are a work in progress; please feel free to share your critiques! > > Terminology is from [[Martin Fowler]]'s [Test Double](https://martinfowler.com/bliki/TestDouble.html) article. To get the best signal from our tests, we should test at the highest level of [[Fidelity (test)]] possible. Therefore, we should use the real, production code if we can. However, sometimes the real thing causes us problems! It can be: * Too unreliable, leading to flaky tests * Too slow, so you get slow feedback * Unable to simulate error-states, so we cannot exercise our reliability logic >❗If none of these apply, use the real thing! Then you're certain your tests reflect production behaviour. >❗The downside to using a test-double is that it can differ in implementation from the real thing; both now, or as the real thing changes behaviour. To make sure they behave in the same way, carefully design [[Contract test|Contract test]]s. To test at the highest level of [[Fidelity (test)]], we should use a: 1. The [[Real]] thing 2. [[Mimic|Fake]] 3. [[Mock]] 4. [[Parrot|Stub]] | | [[Real]] | [[Mimic\|Fake]] | [[Mock]] | [[Parrot\|Stub]] | | ----------------------------- | -------- | ------------------------ | -------- | ---------------- | | Fast | Yes/No | Yes | Yes | Yes | | Reliable | Yes/No | Yes | Yes | Yes | | Asserts method invocations | No | No | Yes | No | | Enforces invariants | Yes | Yes | No | No | | Can simulate error-states<br> | (No) | (Yes) | (Yes) | (Yes) | | Debugger can inspect state | Yes | Yes | No^1 | N/A | > 1: Depends on mocking-framework. Not inspectable for [[Mockito]]. Let's say we are working with patient data hosted by the government. Unfortunately, its test-environments are both slow and flaky, so we need a test-double. In a Java-like language, its interface might look like this: ```java public interface PatientRepo { public Patient create(Patient patient); } ``` # [[Stub]] Starting with the simplest test double, a [[Parrot|Stub]] is like a [[Parrot]]. You tell it what to say, and it repeats it back to you. An example implementation might be: ```java public class PatientStub { public Patient create(Patient patient) { return Patient(123); } } ``` Note that it doesn't contain any logic. Stubs are helpful because they are super easy to implement, e.g. if you're doing [[Top-down design]] and just need a temporary implementation for your first tests. They can also easily grow into a fully qualified [[Mimic|Fake]]. # [[Mock]] Like [[Mock|Tracker]]s; not only do they return pre-defined responses, they also track method calls (and/or the lack of them). ```java @Test void createsPatientOnce() { var repo = mock(PatientRepo.class); var patient = new Patient(123); when(patientRepo.create(any(Patient.class))).thenAnswer(invocation -> { Patient patient = invocation.getArgument(0); return new Patient(patient.id()); }); repo.create(patient); verify(repo, times(1)).create(patient); } ``` Mocks do not enforce invariants. If it turns out the real thing enforces an invariant you weren't aware of, you have to go through every test and make sure. I also dislike mocks because of the power they give you. Most of the time, which and how many times methods are called should be an implementation detail, hidden away from the test. In select cases, method call counts can be helpful for performance optimisation, but these can also be tracked by fully fledged fakes. # [[Mimic|Fake]] Can contain as much logic as you want; they are as much like the real thing as possible, while staying fast and reliable. ```java public class FakePatientRepo implements PatientRepo { private final Map<int, Patient> patients = new HashMap<>(); @Override public Patient create(Patient patient) { if (patients.containsKey(patient.id())) { throw new IllegalArgumentException( "Patient with ID " + patient.id() + " already exists" ); } // Copy to keep item in map immutable patients.put(patient.id(), patient.copy()); // Copy to keep item in map immutable return patient.copy(); } } ``` If you discover another invariant, you can easily add it. Anywhere this invariant is violated in your code, the tests will fail. Fakes can also be written so they can exercise your error-logic. ```java public class FakePatientRepo implements PatientRepo { private final Throwable shouldThrow; public FakePatientRepo(Throwable shouldThrow) { this.shouldError = shouldError; } @Override public Patient create(Patient patient) { if (shouldThrow != null) { throw shouldThrow; } ... } } ``` You can then initialise it in your tests to check your error-handling. ### References * Google [Testing on the Toilet]() --- Q. How does a [[Mimic]] differ from a [[Parrot]]? A. A fake is a simple implementation for tests, a mock has no implementation, only pre-specified responses. Q. When would you use a [[Parrot]]? A. 1) When under short-term time-pressure or for throwaway code, and 2) Dummy-creation, where the contents of the dummy don't matter.