34

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 male? and age.

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)?

  • 3
    Well, one of the points is, that by mocking / unit-testing it, you can isolate any problems to your code. If an integration test fails, you are unaware as to whose code is broken, yours, or the API. – Chris Wohlert Jun 6 '16 at 10:45
  • 9
    Only four tests? You have six boundary ages you should be testing: 17, 18, 19, 20, 21, 22... ;) – David Arno Jun 6 '16 at 10:48
  • 22
    @FilipBartuzi, I assume the method is checking whether a male is over 21 for example? As currently written, it doesn't do that, it is only true if they are 22+. "Over 21" in English though means "21+". So there is a bug in your code. Such bugs are captured by testing boundary values, ie 20, 21, 22 for a male, 17,18,19 for a female in this case. So at least six tests are needed. – David Arno Jun 6 '16 at 10:54
  • 6
    Not to mention the cases of 0 and -1. What does it mean for a person to be -1 years old? What should your code do if your API returns something nonsensical? – RubberDuck Jun 6 '16 at 13:36
  • 9
    This would be a lot easier to test if you passed a person object as the parameter. – JeffO Jun 6 '16 at 13:42
72

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.

  • 6
    "we can get a pretty confident assessment that the system is doing what it should, without drowning in alternative scenarios to test." Thank you. I love when someone approaches automated testing with sanity. – jpmc26 Jun 7 '16 at 2:58
  • 1
    J.B. Rainsberger has a nice talk about tests and the combinatorial explosion you're writing about in the last paragraph, called "Integrated Tests Are A Scam". It's not so much about integration tests, but still quite interesting. – Bart van Nierop Jun 8 '16 at 15:34
  • 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 -> That is very clever mindset, thanks! The problem is when you do project for yourself. It's hard to split your mindset between being a programmer and being a product manager at the same moment – Filip Bartuzi Jun 16 '16 at 8:23
12

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 Person based on their name. This requires integration testing, to make sure it can find Person objects which are presumably created/stored elsewhere.
  • Calculating whether a Person is 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::API.new (or 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.

11

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.

  • does not apply to dynamic languages (i.e. without build stage) – Filip Bartuzi Jun 16 '16 at 8:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.