Unit Tests in C#
Test Doubles
When unit testing, we often have to provide fake implementations of the dependencies of the Service Under Test (SUT). These are called different names, but it’s good to actually differentiate between them and understand the differences between various kinds of test doubles.
- Dummy - the simples possible implementation that is there just to provide it
as an argument, probably to some constructor. Even a
null
could be considered as a dummy sometimes. - Stub - a slight step up from dummies, they implement methods/properties in the most straightforward way, most likely hardcoding return values. The difference from dummy is the fact that with dummies we mostly care about the implementation just being there, without the need of it actually working in any way.
- Spy - a test double that is able to record actions done on it. This way we can validate if some dependency was ever called, or how many times it happened.
- Fake - a stub with more complex implementation than a one-liner hardcoded return value. An example could be a fake that contains some in-memory dictionary that simulates a database.
- Mock - an implementation produced by a mocking library, like Moq or NSubstitute. It can often behave like a dummy, stub, spy, or fake.
Approaches to Testing
When writing tests, it’s good to follow the below advice:
Use Stubs or Fakes with Queries, and Mocks with Commands.
Using Mocks (or Spies) to verify Queries is troublesome, since it introduces interaction-based approach to a test that could be writtn in a state-based manner.
State-based Testing
Preferably, if possible, it’s best to use state-based testing approach, since it does not rely that much on internal implementation of the tested thing. It makes tests less fragile. The tests are fragile when internal changes in the implementation require chanes in the tests, even though the public contract of the tested thing stayed the same.
State-based testing is often the valid option for testing Queries.
Interaction-based Testing
Interaction-based testing is often necessary, but it introduces tight coupling between the test and internal implementation of the tested thing. Tests become fragile, and often need to be changed whenever the implementation internals change.
Interaction-based tests use mocks as spies and stubs, when often we could just be using fakes. It might save us from the need to update tests when SUT changes.
Often, tests that could use state-based approach are written in interaction-based style, making the code over-tested.
Testing internal code
In some of our assemblies we might have internal
classes. It is totally
alright, since we might want just the interface of that class to be public
,
while the implementation should be hidden.
By default, other assemblies (including unit tests projects) will not have
access to internal
classes. We still should test them though. In order to do
that, we can opt-in to make internal classes visible to selected projects.
The following ItemGroup
should be written into the .csproj file that represents
the project with internal
classes:
References
Refactoring tests from interaction-based to State-based (Mark Seemann)