Failure is the first step to trying
The official guidance on test-driven development is to follow the red-green-refactor cycle:
- Write a test that fails.
- Make it pass.
- 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.
- 类别:
- Testing
评论
要发表评论,请点击 此处 登录到您的 MathWorks 帐户或创建一个新帐户。