1
+ %% MATLAB xUnit Test Framework: Architectural Notes
2
+ % This document summarizes the key classes and design choices for MATLAB xUnit,
3
+ % a MATLAB unit testing framework based on xUnit patterns.
4
+ %
5
+ % Note: Testing pattern and smell terminology in this document is drawn from
6
+ % _xUnit Test Patterns: Refactoring Test Code_, by Gerard Meszaros,
7
+ % Addison-Wesley, 2007.
8
+
9
+ %% TestComponent, TestCase, and TestSuite
10
+ %
11
+ % <<class_diagram_a.gif>>
12
+ %
13
+ % The abstract |TestComponent| class defines an object that has a description (a
14
+ % name and a location) and that can be run.
15
+ %
16
+ % A |TestCase| object is a test component that defines an individual test case
17
+ % that can be run with a pass or fail result.
18
+ %
19
+ % A |TestSuite| object is a test component that contains a collection of other
20
+ % test components. Note the hierarchical nature of test suites; they can
21
+ % contain both individual test case objects as well as other test suites.
22
+ % Running a test suite means invoking the |run| method on each test component in
23
+ % its collection.
24
+
25
+ %% TestCase: The Four-Phase Test
26
+ %
27
+ % The TestCase class provides the standard xUnit _Four-Phase Test_, using
28
+ % a _Fresh Fixture_, _Implicit Setup_, and _Implicit Teardown_. These all
29
+ % elements can all be seen in the |run| method of TestCase:
30
+ %
31
+ % function did_pass = run(self, monitor)
32
+ % %run Execute the test case
33
+ % % test_case.run(monitor) calls the TestCase object's setUp()
34
+ % % method, then the test method, then the tearDown() method.
35
+ % % observer is a TestRunObserver object. The testStarted(),
36
+ % % testFailure(), testError(), and testFinished() methods of
37
+ % % observer are called at the appropriate times. monitor is a
38
+ % % TestRunMonitor object. Typically it is either a TestRunLogger
39
+ % % subclass or a CommandWindowTestRunDisplay subclass.
40
+ % %
41
+ % % test_case.run() automatically uses a
42
+ % % CommandWindowTestRunDisplay object in order to print test
43
+ % % suite execution information to the Command Window.
44
+ %
45
+ % if nargin < 2
46
+ % monitor = CommandWindowTestRunDisplay();
47
+ % end
48
+ %
49
+ % did_pass = true;
50
+ % monitor.testComponentStarted(self);
51
+ %
52
+ % try
53
+ % self.setUp();
54
+ % f = str2func(self.MethodName);
55
+ %
56
+ % try
57
+ % % Call the test method.
58
+ % f(self);
59
+ % catch failureException
60
+ % monitor.testCaseFailure(self, failureException);
61
+ % did_pass = false;
62
+ % end
63
+ %
64
+ % self.tearDown();
65
+ %
66
+ % catch errorException
67
+ % monitor.testCaseError(self, errorException);
68
+ % did_pass = false;
69
+ % end
70
+ %
71
+ % monitor.testComponentFinished(self, did_pass);
72
+ % end
73
+ %
74
+ % Phase 1 sets up the test fixture via the _Implicit Setup_ call, |self.setUp()|.
75
+ % The base class |setUp()| method does nothing.
76
+ %
77
+ % Phases 2 and 3 (exercising the system under test and verifying the expected
78
+ % outcome) are handled by the test method, which is invoked by |f(self)|.
79
+ %
80
+ % Phase 4 tears down the test fixture via the _Implicit Teardown_ call,
81
+ % |self.tearDown()|. The base class |tearDown()| method does nothing.
82
+ %
83
+ % Test failure and test error exceptions are caught and handled by the |run()|
84
+ % method, so test methods do not need to use try-catch. This facilitates
85
+ % simple, straight-line test-method code.
86
+ %
87
+ % _Note: The |monitor| object will be discussed later._
88
+
89
+ %% Test Case Discovery
90
+ % The static method |TestSuite.fromName| constructs a test suite based on the
91
+ % name of an M-file. If the M-file defines a |TestCase| subclass, then |fromName|
92
+ % inspects the methods of the class and constructs a |TestCase| object for each
93
+ % method whose name begins with "[tT]est". If the M-file does not define a
94
+ % |TestCase| subclass, then |fromName| attempts to construct either a simple
95
+ % procedural test case or a set of subfunction-based test cases. (See the next
96
+ % section).
97
+ %
98
+ % The static method |TestSuite.fromPwd| constructs a test suite by discovering
99
+ % all the test cases in the present working directory. It discovers all
100
+ % |TestCase| subclasses in the directory. In addition, it constructs test suites
101
+ % from all the procedural M-files in the directory beginning with "[tT]est".
102
+ %
103
+ % The _File System Test Runner_, |runtests|, provides convenient syntaxes for
104
+ % performing test case discovery automatically.
105
+
106
+ %% FunctionHandleTestCase: For the Procedural World
107
+ % Most MATLAB users are much more comfortable with procedural programming. An
108
+ % important design goal for MATLAB xUnit is to make it as easy as possible for MATLAB
109
+ % users with little object-oriented programming experience to create and run
110
+ % their own tests. The FunctionHandleTestCase supplies the plumbing necessary
111
+ % to support procedural test functions:
112
+ %
113
+ % <<class_diagram_b.gif>>
114
+ %
115
+ % Private properties |SetupFcn|, |TestFcn|, and |TeardownFcn| are procedural
116
+ % _function handles_ (similar to function pointers or function references in
117
+ % other languages).
118
+ %
119
+ % |runTestCase()| is the test method used for constructing a TestCase object.
120
+ %
121
+ % Managing test fixtures requires special consideration, because procedural
122
+ % function handles don't have access to object instance data in order to access
123
+ % a test fixture.
124
+ %
125
+ % The overridden |setUp()| method looks at the number of outputs of the function
126
+ % handle |SetupFcn|. If it has an output argument, then the argument is saved
127
+ % in the private |TestData| property, and |TestData| is then passed to both
128
+ % |TestFcn| and |TeardownFcn| for their use.
129
+
130
+ %% Writing Procedural Test Cases
131
+ % Procedural test cases can be written in two ways:
132
+ %
133
+ % * A simple M-file function that is treated as a single test case
134
+ % * An M-file containing multiple subfunctions that are each treated as a test case.
135
+ %
136
+ % In either case, the test
137
+ % case is considered to pass if it executes without error.
138
+ %
139
+ % Writing one test case per file is not ideal; it would lead to either zillions
140
+ % of tiny little test files, or long test methods exhibiting various bad test
141
+ % smells (_Multiple Test Conditions_, _Flexible Test_, _Conditional Test Logic_,
142
+ % _Eager Test_, _Obscure Test_, etc.) So we need a way to write multiple test
143
+ % cases in a single procedural M-file. The natural MATLAB way would be to use
144
+ % subfunctions.
145
+ %
146
+ % However, subfunction-based test cases require special consideration. Consider
147
+ % the following M-file structure:
148
+ %
149
+ % === File A.m ===
150
+ % function A
151
+ % ...
152
+ %
153
+ % function B
154
+ % ...
155
+ %
156
+ % function C
157
+ % ...
158
+ %
159
+ % function D
160
+ % ...
161
+ %
162
+ % The first function in the file, |A|, has the same name as the file. When
163
+ % other code outside this function calls |A|, it is this first function that
164
+ % gets called. Functions |B|, |C|, and |D| are called _subfunctions_.
165
+ % Normally, these subfunctions are only visible to and can only be called by
166
+ % |A|. The only way that code elsewhere might be able to call |B|, |C|, or |D|
167
+ % is if function |A| forms handles to them and passes those handles out of its
168
+ % scope. Normally this would be done by returning the function handles as
169
+ % output arguments.
170
+ %
171
+ % Note that no code executing outside the scope of a function in A.m can form
172
+ % function handles to |B|, |C|, or |D|, or can even determine that these
173
+ % functions exist.
174
+ %
175
+ % This obviously poses a problem for test discovery!
176
+ %
177
+ % The MATLAB xUnit solution is to establish the following convention for
178
+ % subfunction-based tests. The first function in a test M-file containing
179
+ % subfunction tests has to begin with these lines:
180
+ %
181
+ % === File A.m ===
182
+ % function test_suite = A
183
+ % initTestSuite;
184
+ % ...
185
+ %
186
+ % |initTestSuite| is a _script_ that runs in the scope of the function |A|.
187
+ % |initTestSuite| determines which subfunctions are test functions, as well as setup
188
+ % or teardown functions. It forms handles to these functions and constructs a
189
+ % set of FunctionHandleTestCase objects, which function |A| returns as the
190
+ % output argument |test_suite|.
191
+
192
+ %% TestRunMonitor
193
+ % The abstract |TestRunMonitor| class defines the interface for an object that
194
+ % "observe" the in-progress execution of a test suite. MATLAB xUnit provides two
195
+ % subclasses of |TestRunMonitor|:
196
+ %
197
+ % * |TestRunLogger| silently logs test suite events and captures the details of
198
+ % any test failures or test errors.
199
+ % * |CommandWindowTestRunDisplay| prints the progress of an executing test suite
200
+ % to the Command Window.
201
+ %
202
+ % <<class_diagram_c.gif>>
203
+ %
204
+ % A TestRunMonitor is passed to the |run()| method of a TestComponent object.
205
+ % The |run()| method calls the appropriate notification methods of the
206
+ % monitor.
207
+ %
208
+ % Here is the output when using the CommandWindowTestRunDisplay object on the
209
+ % MATLAB xUnit's own test suite:
210
+ %
211
+ % runtests
212
+ % Starting test run with 92 test cases.
213
+ % ....................
214
+ % ....................
215
+ % ....................
216
+ % ....................
217
+ % ............
218
+ % PASSED in 7.040 seconds.
219
+
220
+ %% File System Test Runner
221
+ % MATLAB xUnit provides a command-line _File System Test Runner_ called
222
+ % |runtests|. When called with no input arguments, |runtests| gathers all the
223
+ % test cases from the current directory and runs them, summarizing the results
224
+ % to the Command Window. |runtests| can also take a string argument specifying
225
+ % which test file, and optionally which specific test case, to run.
226
+
227
+ %% Test Selection
228
+ % Test selection is supported in |runtests| by passing in a string of the form:
229
+ %
230
+ % 'Location:Name'
231
+ %
232
+ % or just:
233
+ %
234
+ % 'Location'
235
+ %
236
+ % Both of these forms are handled by |runtests| and by |TestSuite.fromName|.
237
+ %
238
+ % 'Location' is the name of the M-file containing test cases. 'Name' is the
239
+ % name of a specific test case. Normally, the name of the test case is the name
240
+ % of the corresponding TestCase method. For FunctionHandleTestCase objects,
241
+ % though, 'Name' is the subfunction name.
242
+
243
+ %% Assertion Methods
244
+ % MATLAB xUnit provides the following assertion methods:
245
+ %
246
+ % * _Stated Outcome Assertion_ (|assertTrue|, |assertFalse|)
247
+ % * _Equality Assertion_ (|assertEqual|)
248
+ % * _Fuzzy Equality Assertion_ (|assertElementsAlmostEqual|, |assertVectorsAlmostEqual|)
249
+ % * _Expected Exception Assertion_ (|assertExceptionRaised|)
250
+ %
251
+ % Assertion functions are provided via globally accessible names (e.g.,
252
+ % |assertEqual|). The assertion functions could be moved to the |xunit|
253
+ % package, but MATLAB users are not accustomed yet to packages and package
254
+ % name-scoping syntax.
255
+ %
256
+ % 'message' is the last input to the assertion functions and is optional. (See
257
+ % below for discussion of _Assertion Roulette_.)
258
+ %
259
+ % The _Expected Exception Assertion_, |assertExceptionRaised| is used by forming
260
+ % an anonymous function handle from an expression that is expected to error, and
261
+ % then passing that function handle to |assertExceptionRaised| along with the
262
+ % expected exception identifier. For example:
263
+ %
264
+ % f = @() sin(1,2,3);
265
+ % assertExceptionRaised(f, 'MATLAB:maxrhs')
266
+ %
267
+ % By using this mechanism, test writers can verify exceptions without using
268
+ % try-catch logic in their test code.
269
+
270
+ %% Stack Traces and "Assertion Roulette"
271
+ % _xUnit Test Patterns_ explains the smell _Assertion Roulette_ this way: "It is
272
+ % hard to tell which of several assertions within the same test method caused a
273
+ % test failure.
274
+ %
275
+ % MATLAB xUnit mitigates against _Assertion Roulette_ by capturing the entire stack
276
+ % trace, including line numbers, for every test failure and test error. (The
277
+ % MATLAB MException object, which you obtain via the |catch| clause, contains
278
+ % the stack trace.) The stack trace is displayed to the Command Window, with
279
+ % clickable links that load the corresponding M-file into editor at the
280
+ % appropriate line number.
281
+ %
282
+ % Stack traces can be pretty long, though. Also, test framework plumbing tends
283
+ % to occupy the trace in between the assertion and the user's test code, thus
284
+ % making the trace hard to interpret for less-experienced users. MATLAB xUnit,
285
+ % therefore, uses a stack filtering heuristic for displaying test fault traces:
286
+ % Starting at the deepest call level, once the trace leaves MATLAB xUnit framework
287
+ % functions, all further framework functions are filtered out of the stack
288
+ % trace.
289
+ %
290
+ % Here's an example of stack trace display in the output of |runtests|:
291
+ %
292
+ % <html>
293
+ % <tt>
294
+ % >> runtests testSample<br />
295
+ % Starting test run with 1 test case.<br />
296
+ % F<br />
297
+ % FAILED in 0.081 seconds.<br />
298
+ % <br />
299
+ % ===== Test Case Failure =====<br />
300
+ % Location: c:\work\matlab_xunit\architecture\testSample.m<br />
301
+ % Name: testMyCode<br />
302
+ % <br />
303
+ % c:\work\matlab_xunit\architecture\testSample.m at <span style="color:blue;
304
+ % text-decoration:underline">line 6</span><br />
305
+ % <br />
306
+ % Input elements are not all equal within relative tolerance: 1.49012e-008<br />
307
+ % <br />
308
+ % First input:<br />
309
+ % 1<br />
310
+ % <br />
311
+ % Second input:<br />
312
+ % 1.1000<br />
313
+ % </tt>
314
+ % </html>
315
+ %
316
+ % Clicking on the blue, underlined link above loads the corresponding file into
317
+ % the editor, positioned at the appropriate line.
318
+
319
+ %% Extending the Framework
320
+ % The MATLAB xUnit framework can be extended primarily by subclassing |TestCase|,
321
+ % |TestSuite|, and |TestMonitor|.
322
+ %
323
+ % |TestCase| can be subclassed to enable a new set of test cases that all share
324
+ % some particular behavior. The MATLAB xUnit Test Framework contains three
325
+ % examples of extending |TestCase| behavior in this way:
326
+ %
327
+ % * |FunctionHandleTestCase| provides the ability to define test cases based on
328
+ % procedural function handles.
329
+ % * |TestCaseInDir| defines a test case that must be run inside a particular
330
+ % directory. The |setUp| and |tearDown| functions are overridden to change the
331
+ % MATLAB working directory before running the test case, and then to restore the
332
+ % original working directory when the test case finished. The class is used by
333
+ % the framework's own test suite.
334
+ % * |TestCaseInPath| defines a test case that must be run with a particular
335
+ % directory temporarily added to the MATLAB path. Its implementation is similar
336
+ % to |TestCaseInDir|, and it is also used by the framework's own test suite.
337
+ %
338
+ % |TestSuite| could be similarly extended by subclassing. This might a provide a
339
+ % way in the future to define a test suite containing collections of test
340
+ % components in separate directories, which is not currently supported.
341
+ %
342
+ % Finally |TestRunMonitor| could be subclassed to support a variety of test
343
+ % monitoring mechanisms, such as what might be required by a _Graphical Test
344
+ % Runner_.
0 commit comments