5

I have a question regarding proper design of tests.

Suppose that there is a class A

class A{
public:
       virtual int methodA();
       virtual int methodB();
       virtual int methodC();
       virtual int methodD();
};

And class B

class B: public A{
public:
       int methodA();
       int methodB();
       int methodC();
       int methodD();
};

Suppose that the class B overrides only one method (or small number of methods).

How should the tests be designed? I can create a test fixture for class A and separate test fixture for class B. The problem is that I have a huge code duplication because a lot of tests will be the same for class A and class B. (Because a lot of methods are not overriden so they will have the same behavior.)

  • 1
    It’s worth noting that favoring composition over inheritance makes this a moot question, though inheritance may be required for your use case :-) – gntskn Jun 2 '19 at 0:02
  • @gntskn If B didn't inherit from A but instead had a reference to it, how would you design the tests for A and B? It seems to me, the question (as well as the answer) wouldn't change at all. – doubleYou Jun 2 '19 at 9:13
7

You're focusing on the wrong code.

The point of the test isn't to prove that A or B do what they are supposed to do. The point of the test is to test that clients of A and B get what they need.

That's why you can write a test having no idea what C looks like and C should still pass this test. You can write the test before writing A, B, or C. That's because this isn't about A, B, or C. It's about what uses them. What uses them doesn't care what methods got overridden.

A, B, and C are full of implementation details. That's not what is being tested. What's being tested is that, regardless of implementation, the expectations of client code are met. The test exists to document and enforce those expectations.

Write the test against the abstraction the client code needs. Not against what A, B, or C actually are.

Do this right and when A, B, or C fail the test you won't be rewriting the test. You'll be rewriting them.

  • Thank you for your comment. As far I understand the focus of testing should be moved into more "functional" or "acceptance" testing? What about simply unit tests which should test "implementation details" of each class? Should not be written? Or how should be written? – Newbie Jun 1 '19 at 8:35
  • You never ever test implementation details. You shouldn't even write comments that focus on implementation details. You can write very small classes and very small functions that ensure that testing can be very focused on simple abstractions. But implementation details should be hidden. Even from tests. – candied_orange Jun 1 '19 at 8:41
  • @candie In the implementation, detailing which algorithms you used and why, unless fairly obvious, is good for the next time someone looks at it for maintenance. That is if the code cannot be refactored to make it obvious. Otherwise, yes, nobody better care. – Deduplicator Jun 1 '19 at 15:58
  • I want to push back on “the tests shouldn’t care if B derives A.” It seems that you are describing TDD. In TDD, if no one cares that B derives A, then B should not derive A. I’m of the opinion that avoiding inheritance is almost always the right decision personally, but that doesn’t help describe what to do when you do have tests to share between a class and a subclass. – gntskn Jun 1 '19 at 23:59
  • 1
    @gntskn: The tests interact with the code under test the same way the clients of that code do - through the public interface of the layer/component/subsystem. If the clients are supposed to be decoupled from B, then they, and the tests, cannot know about B. Otherwise you cannot refactor, because the tests prevent you (and if you don't have tests, or if you have to change tests to change the code, then by definition it isn't refactoring, it's something else). You can still test all the code in B, you just have to do it through A; but that constraint comes from design & architectural decisions – Filip Milovanović Jun 2 '19 at 8:27
3

Since any test written for objects of type A works also for objects of type B, you can simply reuse all the tests for all methods you expect to behave similar for both classes. Obviously, you have to write new tests for those methods for which you expect a different behaviour.

The test fixtures for both classes could, for example, derive from a common base class where you put the common code, parametrized by an object of type A. As an alternative, they could use a helper class for the common code. What's most appropriate is case-dependent and should be discussed in terms of code examples (which could make a good question on Stackoverflow or Codereview.SE).

  • Write new tests for those methods you expect to behave differently? Nearly, it's rather additional tests, considering the LSP. – Deduplicator Jun 1 '19 at 16:02
  • @Deduplicator: only if the design follows the LSP, and the tests strictly test nothing but the contract for the LSP. – Doc Brown Jun 1 '19 at 17:14
3

If your design complies with Liskov's Substitution Principe (LSP) (aka the L in SOLID) you can assume that class B shall pass the tests of class A. You may then structure your tests as follows:

  • make a first battery of tests for class A. These tests should check if the class fulfils its contrat, i.e. that for every method, given some preconditions, the expected post-conditions are met, the expected results are obtained, and class invariants remain true.
  • class A and class B must both pass this battery of tests according to the LSP, since B is an A.
  • make a second battery of tests for class B. These tests should check if class B fulfils its more specialised contract, where precondition, postconditions and invariants are specialised. Unchanged behavior does not need specific tests, since it was already tested with the first battery of tests.

If your design does not comply with LSP, you can unfortunately not assume that class B should pass the tests of class A. You would then have to write redundant test cases to verify for each class and method if it complies with its specifications.

Important edit/clarification:

In case the wording above is not clear ( see comments and dv), I’d like to emphasize that I do not recommend to do less test, but on the contrary, to use design properties to make more tests (with the same effort or less).

The idea expressed in the second bullet means that class B is tested against all the contracts promised by class A. If poor design or implementation errors are infringing the LSP, this will immediately be noticed in the first round of tests (failure of test cases).

Please note as well that the proposed strategy does not rely on the implementation details of B. It solely relies on the contracts that B should ensure, regardless of how they are implemented.

The main message here, is that the L of SOLID is not just a conceptual nice-to-have, but a powerful approach to make software easier to test and hence to maintain.

  • 1
    Since a large part of the role of tests is to detect regressions and leaks in the design, my instinct is that it would be beneficial to always test all of A’s contract against B. That way, you’ll know as soon as one of your code changes breaks the LSP. – gntskn Jun 1 '19 at 23:57
  • 1
    @gntskn That’s exactly what I say in the second bullet: B must pass all the tests of A - so fulfill the A contract (it is not sufficient to believe that the design is LSP: test evidence must confirm it). Note also that B passing all the tests is necessary but not sufficient to claim LSP (typically infringements of the history constraints might only be detected if combining B operations with A operations to find out that there are unauthorized side effects). – Christophe Jun 2 '19 at 6:51
  • 1
    Your third point is spot on, and is totally eluded from @candied_orange's answer. – user44761 Jun 3 '19 at 9:24
  • @Christophe I see we’re in agreement — yay for ambiguities in the English language :-) – gntskn Jun 6 '19 at 12:23

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.