Testing interaction with dependencies
Should unit tests assert the inputs to dependencies?
Answering the question in the title, it's a clear yes.
To make that visible, let's use the example of a NameReportingService
which takes in the name of a person, and is expected to write two things to the database: the person's initial, and the amount of letters in the name. No returned value.
public class NameReportingService
{
public void Report(string name)
{
myDatabase.AddInitial(name.Substring(0,1);
myDatabase.AddLetterCount(name.Length);
}
}
// e.g.
myNameReportingService.Report("Bob");
How would you test this method, if not by checking that for input "Bob"
, a mocked database dependency would've received "B"
on its AddInitial
method and 3
on its AddLetterCount
method?
Now you might argue that if the method also returns the same values in a result object, that you can just test this result object instead of the mocked dependency. But then you are blindly assuming that the method ensures that it posts the same values to the database that it also returns to the caller, and testing strategies are the antithesis of blind assumption.
Unit vs integration testing
tl;dr: Is it a unit test or an integration test, to ensure that the inputs to a mocked dependency were correct?
The mocked nature of the dependency directly implies that this is a unit test. If you were using a real dependency, then it'd be an integration test.
This is the very definition of unit and integration tests. A unit test only has one real component (i.e. the unit itself) where everything else is mocked, and an integration test has at least more than one real component to it. "Integration" specifically focuses on the interaction between components, which inherently means you need more than one of them.
Unit OR integration testing?
Discussing the above with a colleague, I was told that the latter is an integration test, and if I've made a mistake with the parameters to SuperWeather I should let the person doing integration tests find this out.
The goal of unit tests is being able to spot at a glance which unit failed. But your colleague's proposed test would make it impossible to spot the difference between:
- The real weather component not yielding the correct output for the given input
- The
ReturnsTemperature
method passing the wrong input to the real weather component
Without a unit test confirming that ReturnsTemperature
calls its dependency correctly, you cannot figure out the difference. But with that unit test, if it passes, then you know that the issue lies with the weather component.
In essence, the issue here boils down to a question that is asked (in many subtle forms) here all the time: why bother writing unit tests, if integration tests already prove that my code is working?
The short answer here is that you don't write tests to be informative when they pass, you write them to be informative when they fail. It's precisely when test failures are encountered that you need the kind of information that localizes the source of the issue.
And when no failure is encountered, we assume that this means the code is working. I say assume, because tested codebases are not 100% bugfree all the time, it's just that the remaining bugs weren't already being tested for.
By themselves, integration tests are incapable of localizing the source of an issue. A failure in an integration test only reveals that "one of these real components failed". Which one? Why?
There might be particular assert failures that inherently point to a specific component, but that is a situational windfall and not something you can expect or rely on.
The basic issue with relying on integration tests as your sole source of testing is that while it will indeed indicate that something isn't working, it's incapable of telling you precisely what isn't working, which means you need to inspect the entire call stack and step through it to find what is going wrong.
By themselves, unit tests are incapable of confirming that the combined application works as expected (i.e. the composition of the individual components that the application is made up of). Unit tests tests individual parts, not how these parts are used as a cog in the larger machine.
Using a simple example, take a screw and a bolt. You can unit test the screw to your heart's content and find no fault in it, it's a fully working screw. You can do the same for the bolt and conclude that it is a fully working bolt.
But if one of them has a reverse thread and the other doesn't, you cannot use them together. Individually, they are valid components, they just don't quite work together.
It's an oversimplified example, but it shows the basic premise of why you need integration testing on top of unit testing.
In conclusion, unit tests are only superfluous if you always write correct integration tests, all needed integration tests, and they never ever fail. But if you hold yourself to that (ridiculous) standard, you'd be writing 100% perfect code and all of your testing effort would have been "wasted" in that sense. It's a nonsensical standard built on the presumption that your code will pass and that you therefore won't need to debug or step through the code, which is the same kind of presumption that is made by people who advocate against testing as a whole, albeit less extreme.
In any real world standard, failures are common and are precisely why we write tests so we can easily spot the failures. By skipping out on unit testing, you do a half-assed testing job. You have some confirmation that the code works or doesn't, but you haven't done anything to help yourself in cases where it doesn't. And let's not pretend like we never have any issues in our code during the development phase.