Discover more from Optivem Journal
Unit Testing Use Cases or Domain?
Should we unit-test Use Cases? Or should we unit-test the Domain? Should we mock out the Database? Or use the real Database? At what levels should we test?
This is a FREE article for Dec 2022. Writing - Status Updates: INSIDER subscriptions are paused and planned to be continued from Feb 2023. Technical Coaching - Status Update: I take on a limited number of coaching clients, so any reservations are generally months done in advance. Most slots have already been filled up for 2023; however, a few open ones remain. So if you’ve been thinking about working with me, you can book a call and help your teams deliver higher-quality software faster.
Should we unit-test Use Cases? Or should we unit-test the Domain?
Should we mock out the Database? Or use the real Database?
At what levels should we test?
Approach #1. Unit Testing Use Cases (Uncle Bob)
In Clean Architecture (Uncle Bob), the use cases are independent of any out-of-process dependencies, i.e., independent of the database, SMPT, messaging bug, and third-party systems. We can thus unit-test the use cases by mocking out any out-of-process dependencies.
For example, see the section “Testable Architectures”:
… you should be able to unit-test all those use cases withut any of the frameworks in place… You shouldn’t need the database connected to run your tests.
This is also demonstrated in his Clean Coders videos “London vs. Chicago“, whereby Uncle Bob demonstrates unit testing the use cases by mocking out any out-of-process dependencies (e.g., the database).
We can also see that he does NOT unit test the domain entities; rather the unit testing is done at a single level, the level of use cases only.
P.S. You can watch my introductory YouTube videos too regarding this approach; see Foundations of TDD & Clean Architecture.
Approach #2. Integration Testing Use Cases & Unit Testing Domain (Khorikov)
In Unit Testing (Vladimir Khorikov), Khorikov distinguishes between managed out-of-process dependencies (e.g., database) versus unmanaged out-of-process dependencies (e.g., SMTP, message bus, third-party systems).
Use Cases are implemented through Controllers (Application Service Layer). These controllers have a direct dependency on the database (e.g., reference the ORM), and interfaces are used only for unmanaged dependencies. The reason is that in this approach, we mock out only unmanaged dependencies but do NOT mock out managed dependencies, i.e., the database is not mocked out. See Don’t mock your database, it’s an implementation detail.
This has a profound impact on the testing approach. The use cases are then not unit tested - rather they are integration tested (see Figure 8.1):
Integration tests cover controllers, while unit tests cover the domain model and algorithms.
Khorikov presents tests as having a fractal structure to verify behavior at different levels:
… your tests… have a fractical structure; they verify behavior that helps achieve the same goals but at different levels. A test covering an application service checks to see how this service attains an overarching, coarse-grained goal posed by the external client. At the same time, a test working with a domain class verifies a sub-goal that is part of that greater goal.
The two levels of testing presented are (see Figure 5.10):
Integration tests target the use case (i.e., the goal)
Unit tests target the domain classes (i.e., the subgoals)
Comparison of approaches
We can see the following differences:
In Approach #1, we apply a one-level approach for testing the Use Cases. The Use Cases are directly tested via Unit Tests (all out-of-process dependencies are mocked out); the Use Case Unit Tests indirectly cover the Domain (hence there are no Unit Tests targeting the Domain per se).
In Approach #2, we apply a two-level approach for testing the Use Cases. The Use Cases are tested via Integration Tests (unmanaged dependencies are mocked out, but managed dependencies are not mocked out). The Domain is tested via Unit Tests.
Even though I currently use Approach 1 as my "default" approach (foundational introduction - see my YouTube videos Foundations of TDD & Clean Architecture), I see value in both approaches.
Some people might raise the question of optimality, is one approach more optimal or is it rather context-dependent? What are the motivations behind each approach, and what value does each approach provide? Do we choose one or the other, or a hybrid?
So if you want to read more about this in 2023, subscribe below. Looking forward to our journey!