One of the things that I liked immediately about the kata is that it introduces some hard constraints. The most important one is that the entrypoint is fixed; it SHALL conform to some pre-determined contract.
This sort of constraint, common if you are working outside in, is a consequence of the dependency inversion principle. The framework owns the contract, the plugin implements it.
Uncle Bob's Bowling Game kata takes a similar approach...
Write a class named Game that implements two methods...
In other words, you API design has already happened; in earlier work, in a spike, because you are committed to supporting a specific use case.
When you don't have that sort of constraint in place already, the act of writing your tests is supposed to drive the design of your API. This is one of the places where we are really employing sapient testing; applying our human judgment to the question "is the API I'm creating suitable for consumers?".
The Unusual Spending Kata introduces two other constraints; constraints on the effects on which the core logic depends. In order to produce any useful value, the results of the local calculation have to be published, and the kata constrains the solution to respect a particular API to do so. Similarly, a stateless process is going to need input data from somewhere, and that API is also constrained by the kata.
So the programmer engaging in this exercise needs to align their design with the surface of the boundary. Excellent.
Because the write constraint and the read constraint are distinct in this kata, it helps to reveal the fact that isolating the test environment is much easier for writes than it is for reads.
The EmailsUser::email is a pure sink; you can isolate yourself from the world by simply replacing the production module with a null object. I find this realization to be particularly powerful, because it helps unlock one of the key ideas in the doctrine of useful objects -- that if you need to be able to observe a sink in test, it's likely that you also need to be able to observe the sink in production. In other words, much of the logic that might inadvertently be encoded into your mock objects in practice may really belong in the seam that you use to protect the rest of the system from the decision you have made about which implementation to use.
In contrast, FetchesUserPaymentsByMonth::fetch is a source -- that's data coming into the system, and we need to have a much richer understanding of what that data can look like, so that we can correctly implement a test double that emits data in the right shape.
Implementing an inert source is relatively straight forward; we simply return an empty value each time the method is invoked.
An inert implementation alone doesn't give you very much coverage of the system under test, of course. If you want to exercise anything other than the trivial code path in your solution, you are going to need substitutes that emit interesting data, which you will normally arrange within a given test case.
On the other hand, a cache has some interesting possibilities, in so far as you can load the responses that you want to use into the cache during arrange, and then they will be available to the system under test when needed. The cache can be composed with the read behavior, so you get real production behavior even when using an inert substitute.
Caching introduces cache invalidation, which is one of the two hard problems. Loading information into the cache requires having access to the cache, ergo either injecting a configured cache into the test subject or having cache loading methods available as part of the API of the test subject.
Therefore, we may not want to go down the rabbit hole right away.
Another aspect of the source is that the data coming out needs to be in some shared schema. The consumer and the provider need to understand the same data the same way.
This part of the kata isn't particularly satisfactory - the fact that the constrained connection to our database allows the consumer to specify the schema, with no configuration required...? The simplest solution is probably to define the payments API as part of the contract, rather than leaving that bit for the client to design.
No comments:
Post a Comment