Skip to main content
Engineering LibreTexts

6.2: Write Tests to Enable Evolution

  • Page ID
    32394
  • \( \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}}\)

    Intent Protect your investment in the legacy code by imposing a systematic testing program.

     

    Problem

    How do you minimize the risks of a reengineering project, specifically, the risks of:

    • failing to simplify the legacy system,
    • introducing yet more complexity to the system,
    • breaking features that used to work,
    • spending too much effort on the wrong tasks,
    • failing to accommodate future change.

    This problem is difficult because:

    • Impact of changes cannot always be predicted because parts of the system may not be well-understood or may have hidden dependencies.

    • Any change to a legacy system may destabilize it due to undocumented aspects or dependencies.

    Yet, solving this problem is feasible because:

    • You have a running system, so you can determine what works and what doesn’t work.

    • You know which parts of the system are stable, and which are subject to change.

     

    Solution

    Introduce a testing process based on tests that are automated, repeatable and stored.

    Hints

    Well-designed tests exhibit the following properties:

    • Automation. Tests should run without human intervention. Only fully automated tests offer an efficient way to check after every change to the system whether it still works as it did before. By minimizing the effort needed to run tests, developers will hesitate less to use them.

    • Persistence. Tests must be stored to be automatable. Each test documents its test data, the actions to perform, and the expected results. A test succeed if the expected result is obtained, otherwise it fails. Stored tests document the way the system is expected to work.

    • Repeatability. Confidence in the system is increased if tests can be repeated after any change is implemented. Whenever new functionality is added, new tests can be added to the pool of existing tests, thereby increasing the confidence in the system.

    • Unit testing. Tests should be associated to individual software components so that they identify clearly which part of the system they test [Dav95].

    • Independence. Each test should minimize its dependencies on other tests. Dependent tests typically result in avalanche effects: when one test breaks, many others break as well. It is important that the number of failures represent quantitatively the size of the detected problems. This minimizes distrust in the tests. Programmers should believe in tests.

     

    Tradeoffs

    Pros
    • Tests increase your confidence in the system, and improve your ability to change the functionality, the design and even the architecture of the system in a behavior-preserving way.

    • Tests document how artifacts of a system are to be used. In contrast to written documentation, running tests are an always up-to-date description of the system.

    • Selling testing to clients who are concerned by security and stability is not usually a problem. Assuring long term life of the system is also a good argument.

    • Tests provide the necessary climate for enabling future system evolution.

    • Simple unit testing frameworks exist for all the main object-oriented languages like Smalltalk, Java, C++ and even Perl.

    Cons
    • Tests do not come for free. Resources must be allocated to write them.

    • Tests can only demonstrate the presence of defects. It is impossible to test all the aspects of a legacy system (or any system, for that matter).

    • Inadequate tests will give you false confidence. You may think your system is working well because all the tests run, but this might not be the case at all.

    Difficulties
    • A plethora of testing approaches exists. Choose a simple approach that fits your development process.

    • Testing legacy systems is difficult because they tend to be large and undocumented. Sometimes testing a part of a system requires a large and complex set-up procedure, which may seem prohibitive.

    • Management may be reluctant to invest in testing. Here are some arguments in favor of testing:

      • Testing helps to improve the safety of the system.

      • Tests represent a tangible form of confidence in the system functionality.

      • Debugging is easier when automated tests exist.

      • Tests are simple documentation that is always in sync with the application.

    • Developers may be reluctant to adopt testing. Build a business case to show them that tests will not only speed up today’s development, but they will speed up future maintenance efforts. Once we discussed with a developer who spent one day fixing a bug and then three days more checking if the changes he made were valid. When we showed him that automated tests could help him in his daily work to debug his program more quickly, he was finally convinced.

    • Testing can be boring for developers so at least use the right tools. For unit testing, SUnit and its many variants are simple, free and available for Smalltalk, C++, Java and other languages [BG98].

     

    Example

    The following code illustrates a unit test written using JUnit in Java[BG98]. The test checks that the add operation defined on a class Money works as expected, namely that 12 CHF + 14 CHF = 26 CHF.

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

    This satisfies the properties that a test should have:

    • This test is automated: It returns boolean value true if the action is the right one and false otherwise.

    • It is stored: it is a method of a test class. So it can be versioned like any other code.

    • It is repeatable: its initialization part (1) produces the context in which the test can be run and rerun indefinitely.

    • It is independent of the other tests.

    Using tests having these properties helps you to build a test suite for the long term. Every time you write a test, either after a bug fix or adding a new feature, or to test an already existing aspect of the system, you are adding reproducible and verifiable information about your system into your test suite. Especially in the context of reengineering a system this fact is important, because this reproducible and verifiable information can be checked after any change to see if aspects of a system are compromised.

     

    Rationale

    Tests represent confidence in a system, because they specify how parts of the system work in a verifiable way, and because they can be run at any time to check if the system is still consistent.

    Automated tests are the foundation for reengineering.
    Figure \(\PageIndex{1}\): Automated tests are the foundation for reengineering. They establish your confidence in the system, reduce risks, and improve confi- dence in your ability to change the system.

    “... testing simply exposes the presence of flaws in a program; it cannot be used to verify the absence of flaws. It can increase your confidence that a program is correct”

    — Alan Davis, Principle 111 [Dav95]

    Systematic testing is heavily promoted by Extreme Programming [Bec00] one of the basic techniques necessary to be able to adapt programs quickly to changing requirements. Changing legacy systems is risky business. Will the code still work after a change? How many unexpected side-effects will appear? Having a set of automated, repeatable tests helps to reduce this risk.

    • A set of running tests provides confidence in the system. (“Are you really sure this piece of code works?” “Yes, look, here I have the tests that prove it.”)

    • A set of running tests represents reproducible and verifiable information about your system, and is at all times in sync with the application. This in contrast to most of the written documentation, which is typically slightly outdated already the next day.

    • Writing tests increases productivity, because bugs are found much earlier in the development process.

     

    Related Patterns

    Write Tests to Enable Evolution is a prerequisite to Always Have a Running Version. Only with a comprehensive test program in place can you Migrate Systems Incrementally.

    Grow Your Test Base Incrementally and Test the Interface, Not the Implementation introduce a way to incrementally build a test suite while a system is evolving.


    This page titled 6.2: Write Tests to Enable Evolution 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?