Let’s say I have a function (written in Ruby, but should be understandable by everyone):
def am_I_old_enough?(name = 'filip') person = Person::API.new(name) if person.male? return person.age > 21 else return person.age > 18 end end
In unit testing I would create four tests to cover all scenarios. Each will use mocked
Person::API object with stubbed methods
Now it comes to writing integration tests. I assume that Person::API should not be mocked any more. So I would create exactly the same four test cases, but without mocking Person::API object. Is that correct?
If yes, then what’s the point of writing unit tests at all, if I could just write integration tests which give me more confidence (as I work on real objects, not stubs or mocks)?
No, integration tests should not just duplicate the coverage of unit tests. They may duplicate some coverage, but that’s not the point.
The point of a unit test is to ensure that a specific small bit of functionality works exactly and completely as intended. A unit test for
am_i_old_enough would test data with different ages, certainly the ones near the threshold, possibly all occurring human ages. After you’ve written this test, the integrity of
am_i_old_enough should never be in question again.
The point of an integration test is to verify that the entire system, or a combination of a substantial number of components does the right thing when used together. The customer doesn’t care about a particular utility function you wrote, they care that their web app is properly secured against access by minors, because otherwise the regulators will have their asses.
Checking the user’s age is one small part of that functionality, but the integration test doesn’t check whether your utility function uses the correct threshold value. It tests whether the caller makes the right decision based on that threshold, whether the utility function is called at all, whether other conditions for access are satisfied, etc.
The reason we need both types of tests is basically that there is a combinatorial explosion of possible scenarios for the path through a code base that execution may take. If the utility function has about 100 possible inputs, and there are hundreds of utility functions, then checking that the right thing happens in all cases would require many, many millions of test cases. By simply checking all cases in very small scopes and then checking common, relevant or probable combinations f these scopes, while assuming that these small scopes are already correct, as demonstrated by unit testing, we can get a pretty confident assessment that the system is doing what it should, without drowning in alternative scenarios to test.
The short answer is “No”. The more interesting part is why/how this situation might arise.
I think the confusion is arising because you’re trying to adhere to strict testing practices (unit tests vs integration tests, mocking, etc.) for code which doesn’t seem to adhere to strict practices.
That’s not to say the code is “wrong”, or that particular practices are better than others. Simply that some of the assumptions made by the testing practices may not apply in this situation, and it may help to use a similar level of “strictness” in coding practices and testing practices; or at least, to acknowledge that they might be unbalanced, which will cause some aspects to be inapplicable or redundant.
The most obvious reason is that your function is performing two different tasks:
- Looking up a
Personbased on their name. This requires integration testing, to make sure it can find
Personobjects which are presumably created/stored elsewhere.
- Calculating whether a
Personis old enough, based on their gender. This requires unit testing, to make sure the calculation performs as expected.
By grouping these tasks together into one block of code, you can’t run one without the other. When you want to unit test the calculations, you’re forced to look up a
Person (either from a real database or from a stub/mock). When you want to test that the lookup integrates with the rest of the system, you’re forced to also perform a calculation on the age. What should we do with that calculation? Should we ignore it, or check it? That seems to be the exact predicament you’re describing in your question.
If we imagine an alternative, we might have the calculation on its own:
def is_old_enough?(person) if person.male? return person.age > 21 else return person.age > 18 end end
Since this is a pure calculation, we don’t need to perform integration tests on it.
We might also be tempted to write the lookup task separately too:
def person_from_name(name = 'filip') return Person::API.new(name) end
However, in this case the functionality is so close to
Person::API.new that I’d say you should be using that instead (if the default name is necessary, would it be better stored elsewhere, like a class attribute?).
When writing integration tests for
person_from_name) all you need to care about is whether you get back the expected
Person; all of the age-based calculations are taken care of elsewhere, so your integration tests can ignore them.
Another point I like to add to Killian’s answer is that unit tests run very quickly, so we can have 1000s of them. An integration test typically takes longer because it is calling web services, databases, or some other external dependency, so we cannot run the same tests (1000s) for integration scenarios as they would take too much time.
Also, unit tests typically get run at build time (on the build machine) and integration tests run after deployment on an environment/machine.
Typically one would run our 1000s of unit tests for every build, and then our 100 or so high value integration tests after every deployment. We may not take each build to deployment, but that is OK because the build we take to deployment the integration tests will be run. Typically, we want to limit these tests to run within 10 or 15 minutes because we do not want to hold up the deployment too long.
Additionally, on a weekly schedule we may run a regression suite of integration tests that cover more scenarios on the weekend or other down times. These can take longer than a 15 minutes as more scenarios will be covered, but typically no one is working on Sat/Sun so we can take more time with the tests.