Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing the right thing, techniques to avoid duplicate coverage

Consider the following sequence of events:

  1. You write a function A() that does a unit of work
  2. You write a a test for function A() to make sure no bugs are present
  3. You write function B() that uses function A()
  4. You write a a test for function B() to make sure no bugs are present
    4.1 Tests for function B() cover function A() As a result you have at least 2 tests covering some of the same functionality

Question 1: Was it worth it to write tests for function A() to begin with?

  1. You write lots of code
  2. You write a huge regression test that tests program functionality end-to-end
    6.1 Effectively this single regression test repeats vast majority of tests already written

Question 2: By following these steps code contains many tests that cover the same thing more than once. Is there a technique to avoid this?

ASSUME:

For the purposes of this question please assume that B does 2 things, one of which is A in its entirety

void performLifeChoice() { // B()
  if (timeIsRight) {
     askForPromotion();   // A()
  } else {
     goBackToSchool();
  }
}
like image 881
James Raitsev Avatar asked Feb 19 '23 13:02

James Raitsev


2 Answers

My general answer would be yes, it's worth writing unit tests for A(), for 2 reasons:

1) You may end up calling A() from a different context than B(), and in that case, you will be glad to know that A() is working, and avoid test duplication. If you didn't have a test for A() separately, you would end up re-writing what are effectively the same tests twice, for the equivalent branch of code.

2) Relatedly, if you don't test A() individually, you may end up having turtles all the way down. Imagine now that a function C() is calling B() in one of its branches, and D() calls C(), etc... If you follow the path of only testing the higher level function, you'll end up with an integration test which will have to cover a larger and larger set of pre-conditions. Keeping individual units tested with the smallest possible context allows you in principle to avoid that problem.

An implicit conclusion of my answer is that if A() will never, ever be called from a place other than B(), it might be worth making A() private, and at that point, testing may become "optional". One the other hand, keeping that test might still be useful, in that it will help you pinpoint that when B() fails, it's because you broke A(). In the words of Kent Beck, "if a test fails, how many things could be wrong? the closer the answer is to 1, the more "unit-y" the test is." - it's useful to have unit tests which help you pinpoint exactly where in your code things went wrong.

Now how could you go about testing B(), without in effect replicating the tests of A(), with added pre-conditions?

This is where Mocking/Stubbing or similar techniques can come into play. If you consider what B() is doing, it really isn't performing A(), it's acting as a "coordinator", piping conditions to various paths. An adequate test, in my opinion, would be "When TimeIsRight, then B() should call A()", the answer provided by A() is not relevant to B(), it's the responsibility of A(), which your unit test covers.

In that frame, what a test on B() should Assert is that A() is called when TimeIsRight, not what A() returns. Depending on the specifics of your code, you may consider making A() "substitutable" within B(), for instance via an interface, or via a function that is injected in place of A().

like image 172
Mathias Avatar answered Apr 09 '23 08:04

Mathias


As the methods seem to be in the same class,

  • If A() is private, just test B(). A() will be tested as part of B()'s test.

  • If A() is public, don't test it as you test B(). Verify A()'s correctness in a separate test. Then in another test, just check that B() calls A() properly. In your example, it would mean verifying if askForPromotion() was called in timeIsRight circumstances.

Note that things would be slightly different if A() and B() were in separate classes.

By the way, with a TDD approach, the order of your events should be 2. 1. 4. 3. ;)

like image 41
guillaume31 Avatar answered Apr 09 '23 09:04

guillaume31