Skip to main content
Engineering LibreTexts

6.5: Test the Interface, Not the Implementation

  • Page ID
    32397
  • \( \newcommand{\vecs}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \) \( \newcommand{\vecd}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash {#1}}} \)\(\newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\) \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\) \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\) \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\) \( \newcommand{\Span}{\mathrm{span}}\) \(\newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\) \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\) \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\) \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\) \( \newcommand{\Span}{\mathrm{span}}\)\(\newcommand{\AA}{\unicode[.8,0]{x212B}}\)

    Also Known As: Black-Box Testing [Pre94]

    Intent Build up reusable tests that focus on external behavior rather than on implementation details, and thereby will survive changes to the system.

     

    Problem

    How can you develop tests that not only protect your software legacy, but also will continue to be valuable as the system changes?

    This problem is difficult because:

    • Legacy systems have many features that should continue to function as the system evolves.

    • You cannot afford to spend too much time writing tests while reengineering the system.

    • You do not want to waste effort in developing tests that will have to be changed as you change the system.

    Yet, solving this problem is feasible because:

    • The interfaces to the components of the system tell you what should be tested.

    • Interfaces tend to be more stable than implementations

     

    Solution

    Develop black-box tests that exercise the public interface of your components.

    Hints

    • Be sure to exercise boundary values (i.e., minimum and maximum values for method parameters). The most common errors occur here.

    • Use a top-down strategy to develop black-box tests if there are many fine-grained components that you do not initially have time to develop tests for.

    • Use a bottom-up strategy if you are replacing functionality in a very focused part of the legacy system.

     

    Tradeoffs

    Pros
    • Tests that exercise public interfaces are more likely to be reusable if the implementation changes.

    • Black-box tests can often be used to exercise multiple implementations of the same interface.

    • It is relatively easy to develop tests based on a component’s interface.

    • Focusing on the external behavior reduces considerably the possible tests to be written while still covering the essential aspects of a system.

    Cons
    • Back-box tests will not necessarily exercise all possible program paths. You may have to use a separate coverage tool to check whether your tests cover all the code.

    • If the interface to a component changes you will still have to adapt the tests.

    Difficulties
    • Sometimes the class does not provide the right interface to support black-box testing. Adding accessors to sample the state of the object can be a simple solution, but this generally weakens encapsulation and makes the object less of a black box.

     

    Examples

    Let’s look back at the test presented in Write Tests to Enable Evolution. The code we saw earlier was supposed to check whether the add operation defined on a class Money works as expected. However, we see that the assert in line (3) actually depends on the internal implementation of the Money class, because it checks for equality by accessing the parts of equality.

    public class MoneyTest extends TestCase {
     // ...
        public void testSimpleAdd() {
            Money m12CHF= new Money(12, "CHF");              // (1) 
             Money m14CHF= new Money(14, "CHF");
            Money expected= new Money(26, "CHF");
            Money result= m12CHF.add(m14CHF);                // (2)
            assert(result.currency().equals(expected.currency())
                && result.amount() == expected.amount());    // (3)
        }
    }
    

    However, if the class Money would override the default equals operation defined on Object (doing so would also require us to override hashCode), the last assert statement could be simplified and would become independent of the internal implementation.

    public class MoneyTest extends TestCase {
     // ...
        public void testSimpleAdd() {
            Money m12CHF= new Money(12, "CHF");    // (1)
            Money m14CHF= new Money(14, "CHF");
            Money expected= new Money(26, "CHF");
            Money result= m12CHF.add(m14CHF);      // (2)
            assert(expected.equals(result));       // (3)
        }
    }
    

     

    Rationale

    The interface of a component is a direct consequence of its collaborations with other components. Black-box tests therefore have a good chance of exercising the most important interactions of a system.

    Since interfaces tend to be more stable than implementations, black-box tests have a good chance of surviving major changes to the system, and they thereby protect your investment in developing tests.

     

    Known Uses

    Black-Box testing is a standard testing strategy [Som96].

     

    Related Patterns

    Record Business Rules as Tests adopts a different strategy to developing tests which focuses on exercising business rules. This is fine if the components to be tested are the ones that implement the business logic. For most other components, Test the Interface, Not the Implementation will likely be more appropriate.

    Components that implement complex algorithms may not be well-suited to black-box testing, since an analysis of the interface alone may not reveal all the cases that the algorithm should handle. White-box testing [Som96] is another standard technique for testing algorithms in which test cases are generated to cover all possible paths through an algorithm.


    This page titled 6.5: Test the Interface, Not the Implementation is shared under a CC BY-SA license and was authored, remixed, and/or curated by Serge Demeyer, Stéphane Ducasse, Oscar Nierstrasz.

    • Was this article helpful?