A Cake for Kotlin part 2: Making an example of Arithmetic
In the first article in this series, I explained my motivation for wanting to do dependency injection inspired by the Cake Pattern in Kotlin (and other languages without self-types), despite the known issues with that style of coding. I presented a way of doing so that I believe will allow programmers to keep the good things from the Cake, while avoiding some of the problems. The examples in that article, however, were extremely small. Let’s create a (slightly) bigger one and look at some more code.
This time, I will present a set of interdependent services, and discuss how best to deal with various issues, such as circular dependencies, in light of that example. I will also demonstrate how to consolidate test registries to make sure the pattern scales when we have many services.
As my contrived example, I will be creating a series of modules to perform various arithmetic operations, such as addition, subtraction, multiplication, and so on. I’ll try to stick to the following rules for the implementation of the operators:
- The language’s built-in arithmetic operators should not be used.
- Operators that are already implemented should be used where possible. (Dependence between the services is the whole point of the excercize afterall.)
I’ll be using Kotlin, with JUnit 5 for unit testing and MockK for mocking.
What I’m writing here clearly isn’t useful code, but it does create some interesting situations when it comes to module dependencies. These situations are analogue to what happens in real life code bases.
A standalone service: Subtraction
What operator is easiest to start with? Intuitively, perhaps subtraction: Count the number of “steps” between the numbers. As an example, 5–2 becomes counting the steps 4,3,2, which gives the correct result of 3. Those steps can be defined as a range of numbers from 2 to 5, with the end of the range being non-inclusive.
Defining subtraction in this way is a bit more complicated when the result is negative as we need to calculate the sign. But those complications aren’t interesting when it comes to what we’re really working on: demonstrating dependency injection. So for simplicity we’ll just throw an exception if the result would be negative.
We define a Service Definition SubtractionService, with a single function to compute differences. Then, anticipating that we’ll have to depend on this service elsewhere, we define the Service Component SubtractionServiceComponent that defines the subtractionService constant. This constant will be usable by services whose component depends on the SubtractionServiceComponent.
Finally, we define the Service Implementation, which implements SubtractionService. As we don’t depend on any other services, we don’t need to take a registry parameter.
Let’s add a test:
There’s nothing fancy here — there’s no dependencies from the service implementation that need to be mocked, so we just test the service implementation directly without worrying. This is fine. We’ll also create a component registry and a production registry, although they won’t do anything interesting yet:
A service with a dependency: Addition
With subtraction complete, let’s do addition: It can be implemented by multiplying the second term with -1 and using subtraction, which we already implemented. A first attempt might look like this:
The AdditionServiceComponent is inheriting (depending on) the SubtractionServiceComponent, making the subtractionService constant available through the registry parameter in the service implementation. The reason we’re doing it this way will become apparent once we get to testing.
The component and production registries now look like this:
As the addition service has dependencies, we need to pass it a registry parameter. The production registry does this, giving the AdditionServiceImplementation it access to the other service implementations.
You may note that we’ve broken one of our own rules in the implementation of sum: We’ve multiplied term2 with -1. Clearly we should be using a MultiplicationService to do this instead of the operator. We’ll get to that in a moment, but first we’ll test the addition service as it is.
There’s a lot more going on here than in the subtraction test, although most of it is mockk syntax for stubbing and verifying. The most important thing is the definition of the test registry. Here, we state that we want to test the addition service in a context where the subtraction service is mocked out. This is the whole point (well, the most important point) of dependency injection: We want to be able to test one service without testing the services it depends upon. In this small example it may not seem like a big deal, but in larger systems this will allow you to limit the number of corner cases that each test will need to deal with. And if the service you depend on is responsible for calling external APIs or databases, those resources may not be available during your testing at all — making mocking the integration service truly necessary.
There is a principle in testing that we should avoid writing tests that depend on the implementation of the method being tested. Writing unit tests against code with mocked dependencies will often seem to break this principle, until you accept a certain nuance: What we’re actually testing is that the method uses its dependencies in the intended manner, and that it gives the expected result assuming the dependencies behaves as expected. Keep those goals in mind, and you should be able to write effective unit tests for your code.
The test we’ve written for the addition service does this: First it defines, using mockk syntax, the expected behavior of the subtractionService in one very specific case: If given the parameters 2 and -4, it should return the expected result: 6. We then invoke the sum method on the registry that has the mocked dependency, and go to verification: We assert that the method under test gave the expected result. We then do two verifications: First, that the difference-method of the subtraction service was called exactly once with the expected parameters.
(In a perfect world we would also verify that the mock wasn’t called with any other parameters, that no other mocks were called, that no other methods on this mock was called, etc — but this isn’t a mocking tutorial, so we’ll keep it simple.)
What our unit test does not do is test that the service and its dependencies does what was intended in unison. But there’s nothing stopping us from writing that test as well. Understand, however, that this is not a unit test in the strictest sense of the term — it’s actually more of an internal “service integration” test.
Both unit tests and this kind of “internal integration tests” have their place: The latter is good for testing that the major functions of a program combine to give the intended effects, completely regardless of implementation — but when such tests fail, it can be a lot of work to figure out exactly what went wrong. And as we mentioned, it isn’t always possible to write such tests when the code has external dependencies.
Unit tests allow you to home in on a bug directly, as each of them test very little code. They also allow you to test services with dependencies that can’t themselves be tested due to limitations in the build environment. Another benefit of unit tests is that they document how the interaction between services is intended to work. This can be very useful when debugging systems where the responsibilities of each method aren’t as clear-cut as in our example.
Dealing with interdependence: Multiplication
When implementing addition, we multiplied the second term with -1 using the language multiplication operator. Given the rules I set down for the exercise, we should use a multiplication service to do this instead.
Multiplication can be viewed as a series of additions: adding the first factor to itself a number of times equal to the second factor. So let’s implement a multiplication service dependent on the addition service.
We add the multiplication service to the registries, write tests as before (with some more work with mockk to verify that we get several calls to the sum method), and they work as expected. We also add a mock of the multiplication service to the Addition Service unit test, as the testRegistry there complains that it has no implementation of the new service in the component registry.
Back in the addition service, we add a dependency and use it to replace the “term2*-1”:
Great, right? No. The compiler is NOT happy:
e: AdditionService.kt: (5, 66): There’s a cycle in the inheritance hierarchy for this type
e: AdditionService.kt: (11, 71): Unresolved reference: multiplicationService
e: MultiplicationService.kt: (5, 43): There’s a cycle in the inheritance hierarchy for this type
e: MultiplicationService.kt: (12, 50): Unresolved reference: additionService
We’ve been using inheritance to specify dependencies, and as we saw in the previous article, this doesn’t allow cyclical dependencies. But from that article we know how to deal with that: Remove the explicit dependencies and everything should be fine… right?
Removing the explicit dependency from the AdditionServiceComponent and typing the registry parameter of AdditionServiceImpl with the component registry is enough to fix the compilation faults:
The compiler may be happy now, but we soon discover that the tests are not. The unit test on the AdditionServiceImpl says:
no answer found for: MultiplicationService(#3).product(4, -1)
io.mockk.MockKException: no answer found for:
This is fairly easy to understand — I mentioned earlier that we had to add a mock of the multiplication service to this test, but we forgot to add stubbing of the product method. Doing that, the test ends up looking like this:
This test now passes. What about the service integration test, which tests the addition service implementation with its actual dependencies? It now fails:
expected: <6> but was: <2>
The integration test on the multiplication service also fails:
expected: <8> but was: <0>
So, what’s going on? As it’s an integration test failing, the problem could be anywhere. In a larger system, debugging would often be a big job. Luckily our example is small, and we’ll just examine the call stack:
The AdditionIntegrationTest calls sum(2,4). This causes a call to product(4, -1), which again causes a call to sum(0, -1), which causes a call to product(-1,-1), which… fails because (1..factor1) doesn’t work when the right-hand side of the range is less than the left hand side, simply returning an empty range. This causes the recursion to eventually terminate with the wrong answer.
Great, we found a bug! We’ll be good developers and reproduce the problem with a unit test before fixing it:
Apart from a bit of mockk magic to provide a mock implementation of sum, this is fairly straightforward. (Also, I was extremely lazy with the verifications.) The test fails of course, but that’s what we expect (and want) when reproducing a bug:
expected: <-8> but was: <0>
Why do we want the unit test when we already have the integration test which is failing due to this bug? In the future, any mistakes of this kind can now be instantly traced to the multiplication service by the unit test, with no need to examine the recursive call stack to find the problem. This will allow such problems to be found and solved more quickly. Most systems are much more complicated than this example, making quick identification of bugs important for maintainability.
We go back to the multiplication service and try to fix the bug:
The unit test is happy. We run the integration test again, and…
It probably surprises no one that our “clever” two-method recursion never terminates. We didn’t design the methods involved to handle recursion. Still, both in the multiplication service and in the addition service it seemed perfectly reasonable to rely on the other service. In a larger system, where the services might be written by different developers, and where integration testing is much harder than it was here, this could have turned into a major headache — especially if the problem was more subtle than a non-terminating recursion, or if parts of the code could only be unit tested, not integration tested.
While it can be useful to allow interdependent services in some cases, doing so can quickly turn into problems like this one. My advice to you is to be happy that the compiler tells you off when you’re about to introduce a circular dependency — and instead of going ahead and introducing such a dependency like we did here, find another way if at all possible.
Multiplication take two: No circular dependencies!
We go back to the addition service and put back the dependency we removed:
Sure enough, the compiler starts complaining about the circular dependency again. Good compiler, you were trying to warn me about that nasty tangle all the time, weren’t you? I should have listened.
We need to remove either multiplication’s dependence on addition, or vice versa. In this simple example, we could just write a counting-based addition service, like we did with subtraction. But in a more serious system, that kind of rewrite may not be applicable, at least not without duplicating a lot of code between the two services.
An approach that is more generally useful is to extract common functionality to a higher service that both interdependent services can depend on instead of each other. With that in mind, is there something that both the addition service and the multiplication service does?
As it happens, yes: They both multiply with -1. For the addition service, this is the only multiplication it needs. For the multiplication service, it is done to compensate for language issues when the first factor is negative.
At this point, we realize that multiplying with -1 is a very special case of multiplication — it is actually just using the unary minus operator. Let’s write a service for that:
I’ll leave it as an exercise to the reader to find a clever implementation of unary minus that doesn’t depend on the minus operator of the language. It doesn’t matter for the example.
Using this new service, the addition service now look like this:
And the multiplication service looks like this:
Tweaking the mocks a bit, the tests all run green.
The take-away is this: Be extremely careful about introducing circular dependencies. Again for the crowd in the back:
Be extremely careful about introducing circular dependencies.
Look for new services that can be extracted to avoid the need for such cycles. Only introduce them if you are really, really sure they are the least bad solution to your problems.
Consolidating test registries
As we keep adding more services, the length of initializations in the production registry and test registries keep growing. By the time we’ve added services for division and power operators to our example, the addition unit test will look like this:
The number of services in the test registry is growing. Adding another service now means not only registering it in the production registry, but also in the test registries defined in each and every unit test. This clearly isn’t sustainable, we need to find a better solution.
A simple solution is to create a default test registry (residing in the test code folder), that the tests can extend:
We use an open class, not an interface, to make sure the compiler tells us if we forget to mock a new service we’re adding. Using the default test registry, we can rewrite the tests like this:
In this way, each test need only redefine the constants they want the real implementation of — typically the class under test. Introducing a new service only requires us to register it in three places: The component registry, the production registry and the default test registry — it’s a bit of ceremony, but much better than the ever-growing version we had.
We’ve examined a more involved example of the cake pattern as adopted to Kotlin. We have looked at why circular dependencies are dangerous and how to avoid them, and argued why it’s important that the compiler catch such things in larger projects. Finally, we’ve looked at how to consolidate the definition of test registries to avoid having to go into every single test when adding a new test.
Source code for a “completed” version of the project discussed in this article can be found here: https://github.com/sigmanil/mathinjectionkotlin
It should be clear at this point that the “cake without self-types” is at least as expressive as the original cake pattern. It is also safer, allowing compile-time checks to prevent circular dependencies, and to prevent us from forgetting to add mocks to the test registries. In future articles, we aim at examining performance and to compare it with common frameworks that also handle dependency injection for Kotlin.