Developer Zone

Advanced Software Development with MATLAB

Failure is the first step to trying

The official guidance on test-driven development is to follow the red-green-refactor cycle:

  1. Write a test that fails.
  2. Make it pass.
  3. Refactor.

But what’s the point in starting with a test that fails? To make sure you’ve written the right test! I encountered some unexpected behaviour recently that highlighted this point.

Imagine you have a Library that aggregates Items. An Item can be either a Book or a Film, but not both at the same time. If we create a Library in "book mode", it should initially contain an empty Book. If we create it in "film mode", it should initially contain an empty Film. Let’s start by writing a test to capture the book mode behaviour:

classdef tLibrary < matlab.unittest.TestCase

    methods(Test)

        function bookModeInitialisesToEmptyBook(testCase)
            
            lib = Library(Mode="book");
            
            testCase.verifyEqual(lib.Items,Book.empty(1,0))
            
        end
        
    end

end

(The Name=value syntax for name-value pairs was introduced in R2021a. It’s interchangeable with the classic (…,"Name",value) syntax.)

Let’s run the test. We expect it to fail because Library doesn’t exist.

We’ll skip over the steps of creating a blank class definition, the subsequent test failures due to a missing constructor with input arguments and a public Items property, and iteratively adding them in.

Instead, let's jump to the implementation of Library that makes our test pass:

classdef Library
    
    properties
        Items (1,:) Item = Book.empty
    end
    
    methods
        
        function lib = Library(nvp)
            
            arguments
                nvp.Mode (1,1) string {mustBeMember(nvp.Mode,["book" "film"])} = "book"
            end
            
        end
        
    end
    
end

We run the test and see that it passes:

So far, so good. Now we write a test to capture film mode:

function filmModeInitialisesToEmptyFilm(testCase)

    lib = Library(Mode="film");

    testCase.verifyEqual(lib.Items,Film.empty(1,0))

end

We run the test:

And… it passes!?

Why is it passing? We can use the debugger to inspect lib.Items manually and see that it’s an empty Book and not an empty Film. After some investigation, we find that verifyEqual relies on isequal and isequal considers two empties of different classes to be equal under some circumstances.

Whether or not this behaviour of isequal is correct, the important point for us is that we’ve written the wrong test! Our implementation could have been wrong and we wouldn’t have known. We would have achieved full coverage but our test would not have been contributing useful information.

We therefore need to rewrite our test to catch this issue:

function filmModeInitialisesToEmptyFilm(testCase)

    lib = Library(Mode="film");

    testCase.verifyEmpty(lib.Items)
    testCase.verifyClass(lib.Items,?Film)

end

Let’s run the updated test:

The test now fails and we can continue with our implementation, confident that our test will only pass when our implementation is correct.




Published with MATLAB® R2022a

|
  • print

Comments

To leave a comment, please click here to sign in to your MathWorks Account or create a new one.