Test Fixtures, Parameterization and Mocking
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Written against MATLAB R2026b. This page continues Unit Testing with matlab.unittest, which introduces test files, qualifications, tolerances and runtests, and reuses its heatflow functions heatflow.laplacian and heatflow.stableTimeStep. Octave 11.3.0 has no matlab.unittest, matlab.mock or matlab.perftest, so no test class here was run. The only Octave 11.3.0 run is a table of the numerical errors that justify a tolerance in the parameterised example; every class and call is labelled "not run here" and was checked against the R2026b documentation pages listed under Sources.
Setup and teardown
A test fixture is setup and teardown code that puts the system in the pretest state and returns it to its original state afterwards. In a class-based test, four method attribute blocks define it:
| Block | Runs |
|---|---|
|
Before and after each |
|
Once before all the |
Setup methods of a superclass run before those of its subclasses. The documentation recommends that teardown actions are written inside the setup methods with addTeardown, not in TestMethodTeardown or TestClassTeardown blocks. testCase.addTeardown(fcn,arg1,...) registers a function to run when the test case ends, in the reverse order of registration, and it runs even if the test throws an error, which makes the test content exception-safe. Call addTeardown immediately before or after the change that it undoes, with no code between that can throw an exception.
The class below restores the random number generator state and the current display format. The rng call is a state change that would otherwise leak into the next test:
classdef FieldNoiseTest < matlab.unittest.TestCase
% FieldNoiseTest.m (not run here)
properties
Field
end
methods (TestClassSetup)
function fixSeed(testCase)
previous = rng; % save the generator state
testCase.addTeardown(@rng,previous) % restore it afterwards
rng(42,"twister")
end
end
methods (TestMethodSetup)
function createField(testCase)
testCase.Field = 300 + 0.5*randn(21,21);
end
end
methods (Test)
function laplacianKeepsInteriorSize(testCase)
L = heatflow.laplacian(testCase.Field,0.05);
testCase.verifySize(L,[19 19])
end
function noiseIsSmall(testCase)
testCase.verifyLessThan(max(abs(testCase.Field(:) - 300)),5)
end
end
end
The class-level setup runs once; the method-level setup builds a new Field for every test, because the framework gives each test its own copy of the test case object, so a modification of testCase.Field in one test is not seen by another. The documented examples of class-level setup restore a display format in the same way (testCase.addTeardown(@format,originalFormat)), and say that class-level setup is useful when the action is slow and repeating it for every test would be wasteful.
Fixtures
The matlab.unittest.fixtures namespace holds ready-made fixtures for frequent actions. Inside a test, fixture = testCase.applyFixture(fixtureObject) sets up the fixture, returns the object, and tears it down automatically when the test case ends. The documented ones used most:
| Fixture | Does |
|---|---|
|
Creates a temporary folder (property |
|
Adds folders to the path for the duration of the test and restores the path afterwards. |
|
Change the current folder, and suppress given warnings, for the duration of the test. See their reference pages for the exact behaviour and arguments. |
A fixture is also a class, so you can write your own by deriving from matlab.unittest.fixtures.Fixture.
The next test writes a small sensor log to a temporary folder and reads it back. It uses applyFixture, and addTeardown for the file handle, in the pattern of the documentation’s example:
classdef SensorLogTest < matlab.unittest.TestCase
% SensorLogTest.m (not run here)
methods (Test)
function readsHeaderAndRows(testCase)
import matlab.unittest.fixtures.TemporaryFolderFixture
fixture = testCase.applyFixture(TemporaryFolderFixture);
file = fullfile(fixture.Folder,"sensors.csv");
writelines(["time,sensor,temperature" ...
"2026-10-22T08:00:00,S1,301.5" ...
"2026-10-22T08:00:00,S2,299.25"],file)
T = readtable(file);
testCase.verifySize(T,[2 3])
testCase.verifyEqual(T.Properties.VariableNames, ...
{'time','sensor','temperature'})
testCase.verifyEqual(T.temperature,[301.5; 299.25])
end
end
end
The columns are those of the heatflow sensors.csv log. Reading and writing such files is on Reading and Writing Tabular Data.
Source: MATLAB documentation: TemporaryFolderFixture, applyFixture, PathFixture, WorkingFolderFixture, SuppressedWarningsFixture
Shared test fixtures
When several test classes need the same expensive or global setup, declare it as a shared test fixture with the SharedTestFixtures class attribute, instead of in each class’s TestClassSetup. When the classes run together, the framework groups the classes that have exactly the same shared fixtures and sets the fixture up once before the group and tears it down once after it, whereas a fixture applied in TestClassSetup is set up and torn down for every class. The documentation example shares a PathFixture that adds the source folder to the path in two test classes:
classdef (SharedTestFixtures={ ...
matlab.unittest.fixtures.PathFixture(fullfile("..","source"))}) ...
LaplacianPathTest < matlab.unittest.TestCase
% (not run here) the source folder is on the path for this class
methods (Test)
function laplacianIsFound(testCase)
testCase.verifyNotEmpty(which("heatflow.laplacian"))
end
end
end
Running runtests(IncludeSubfolders=true) over the folder then prints the fixture’s setup once, the tests of each class, and the teardown once at the end ("Restored the path to its original state" in the documentation’s output). The folder in the example is relative to the test file, so it works from any current folder. The shared fixtures of a test are listed in the SharedTestFixtures property of its Test element.
Parameterized tests
When tests differ only in their data, parameterise the class instead of copying methods. A property in a properties (TestParameter) block holds the values, and a Test method that takes the property as an input argument is run once for each value. A value can be a cell array or a scalar structure: with a cell array the framework builds descriptive names from the values, types and sizes (such as int16_1 or 3x3_double); with a structure the field names are the parameter names, which gives the suite names you control. Initialise the property with value objects, because tests that share a mutable handle object (a figure) can affect each other; to test handle objects, store function handles that construct them, as the documentation does with @figure and @uifigure.
The example checks heatflow.laplacian for three grid sizes and three known fields. The field’s expected Laplacian travels with it in a structure:
classdef LaplacianParameterizedTest < matlab.unittest.TestCase
% LaplacianParameterizedTest.m (not run here)
properties (TestParameter)
gridSize = struct("small",5,"medium",11,"large",41)
field = struct( ...
"constant", struct("f",@(x,y) 300+0*x, "lap",0), ...
"linear", struct("f",@(x,y) 2*x+3*y, "lap",0), ...
"quadratic",struct("f",@(x,y) x.^2+y.^2,"lap",4))
end
methods (Test)
function outputSize(testCase,gridSize)
L = heatflow.laplacian(ones(gridSize),1/(gridSize-1));
testCase.verifySize(L,[gridSize-2 gridSize-2])
end
function knownLaplacian(testCase,gridSize,field)
h = 1/(gridSize-1);
[X,Y] = meshgrid(0:h:1);
L = heatflow.laplacian(field.f(X,Y),h);
testCase.verifyEqual(L,field.lap*ones(gridSize-2),AbsTol=1e-8)
end
end
end
outputSize produces three tests and knownLaplacian produces nine, because a method with several parameterisation properties is invoked for every combination by default. Test names follow the documented pattern of the method name followed by name=value pairs, for example knownLaplacian(gridSize=small,field=constant), so runtests(pwd,Name="*field=quadratic*") selects the quadratic field for all grids. The tolerance 1e-8 is the result of running the function. This table comes from Octave 11.3.0 (the same arithmetic as the tests):
fields = {@(x,y) 300+0*x, @(x,y) 2*x+3*y, @(x,y) x.^2+y.^2};
names = {"constant","linear","quadratic"};
expect = [0 0 4];
for n = [5 11 41]
h = 1/(n-1);
[X,Y] = meshgrid(0:h:1);
for k = 1:3
L = heatflow.laplacian(fields{k}(X,Y),h);
fprintf("n=%2d %-9s size %dx%d max error %.1e\n", n, names{k}, ...
size(L,1), size(L,2), max(abs(L(:)-expect(k))));
end
end
Octave 11.3.0 output:
n= 5 constant size 3x3 max error 0.0e+00
n= 5 linear size 3x3 max error 0.0e+00
n= 5 quadratic size 3x3 max error 0.0e+00
n=11 constant size 9x9 max error 0.0e+00
n=11 linear size 9x9 max error 3.6e-13
n=11 quadratic size 9x9 max error 9.1e-14
n=41 constant size 39x39 max error 0.0e+00
n=41 linear size 39x39 max error 5.7e-12
n=41 quadratic size 39x39 max error 2.2e-12
The errors grow as the grid is refined (the formula divides by ), up to about , so AbsTol=1e-8 has a safe margin while still catching a real mistake such as a wrong sign. The sizes are exact, so verifySize needs no tolerance.
Levels and combinations
A class can be parameterised at three levels, each with its own property attribute and method attribute. A parameterised method can access the parameters of its own level and of the higher levels.
| Level | Property attribute and method | Accessible properties |
|---|---|---|
Class setup |
|
|
Method setup |
|
|
Test |
|
|
The ParameterCombination method attribute chooses how the parameters of one method are combined:
| Value | Invocations |
|---|---|
|
Every combination of parameter values. |
|
The first values together, then the second values, and so on. All properties must have the same number of values. |
|
Enough combinations that every pair of values of any two properties appears at least once, which gives fewer tests than the exhaustive product. |
Write the attribute in the method block: methods (Test,ParameterCombination="pairwise"). Exhaustive products grow quickly: three parameters of ten values each give 1000 tests, and a pairwise combination gives far fewer.
Parameters that cannot be known when the class is loaded are initialised at suite creation time with a static method that has the combined attributes TestParameterDefinition,Static. Its inputs must be parameterisation properties of the same class (or a superclass) and its outputs too, and the framework builds fresh parameters every time a suite is created. The documentation uses it to enumerate the properties of classes. For the heatflow data folder, one test per CSV file found at suite creation time:
classdef SensorFilesTest < matlab.unittest.TestCase
% SensorFilesTest.m (not run here)
properties (TestParameter)
csvFile
end
methods (TestParameterDefinition,Static)
function csvFile = initializeFiles()
dataFolder = fullfile(fileparts(mfilename("fullpath")),"..","data");
listing = dir(fullfile(dataFolder,"*.csv"));
csvFile = fullfile({listing.folder},{listing.name});
end
end
methods (Test)
function fileIsReadable(testCase,csvFile)
T = readtable(csvFile);
testCase.verifyNotEmpty(T)
end
end
end
Once a parameterisation property has been given a value, either by a default or by a definition method, do not change it afterwards. Filtering is by ParameterProperty and ParameterName in runtests, or by selectors.
Tags and selectors
Tags group tests into categories, such as a feature or a kind of test, so that you can run one category. Tags are strings or character vectors in a cell or string array, set with the TestTags method attribute for individual tests and the TestTags class attribute for every test in the class; a subclass inherits the tags of its superclass.
classdef (TestTags = {"Heatflow"}) TaggedLaplacianTest < matlab.unittest.TestCase
% TaggedLaplacianTest.m (not run here)
methods (Test,TestTags = {"Unit"})
function constantFieldIsFlat(testCase)
L = heatflow.laplacian(300*ones(6,7),0.1);
testCase.verifyEqual(L,zeros(4,5))
end
end
methods (Test,TestTags = {"Slow","Integration"})
function largeGridCompletes(testCase)
L = heatflow.laplacian(rand(2000),0.001);
testCase.verifySize(L,[1998 1998])
end
end
end
There are three ways to select the tagged tests. runtests(pwd,Tag="Unit") selects and runs without creating a suite; testsuite(pwd,Tag="Unit") (and TestSuite.fromClass and fromFile) builds the suite; and the selector classes in matlab.unittest.selectors filter an existing suite with selectIf:
import matlab.unittest.selectors.HasTag
import matlab.unittest.constraints.IsEqualTo
suite = testsuite(pwd);
unitOnly = suite.selectIf(HasTag("Unit"));
untagged = suite.selectIf(~HasTag); % tests with no tag
slowOrInt = suite.selectIf(HasTag(IsEqualTo("Slow") | IsEqualTo("Integration")));
results = run(unitOnly);
(Not run here; the ~HasTag, HasTag(name) and HasTag(constraint) forms are the ones in the documentation’s tag topic.) Tags suit the split used by CI: run the "Unit" tests on every commit and the "Slow" ones nightly. Selectors also exist for names, parameters, procedure names and base folders; runtests exposes the same filters as Name, ProcedureName, ParameterName, ParameterProperty, BaseFolder and Tag.
Mocking with matlab.mock
When a unit depends on something slow, unavailable or not yet written (a database, a network service, a device), a mock object replaces it. A mock implements at least part of the real object’s interface in a simpler, faster or more predictable way, and the component under test cannot tell the difference. The documentation distinguishes two roles, which one mock can play:
-
Stubbing: define the behaviour, for example which value a method returns for given inputs.
-
Spying: intercept and remember the messages the component sends, so that the test can verify that a method was called, how often and with which inputs, or that a property was set.
The workflow is: create the mocks, define their behaviour, run the component, and qualify the interactions. Creating a mock also creates a behavior object, which has the same methods as the mock and is used to define the actions and to qualify interactions. A test class that uses mocks derives from matlab.mock.TestCase, a subclass of TestCase, and calls createMock. The mock is either built from a class (?ClassName, so it implements the abstract methods and properties of that interface) or from nothing with AddedMethods for an implicit interface, which is useful when the real collaborator is not implemented yet.
In the heatflow scenario, an alert routine reads the latest temperature of each sensor from a data source and notifies an operator when a reading exceeds a limit. The source is an abstract interface. As in the documentation’s example, the method is declared Static and abstract, which makes the call form below match the documented one:
classdef SensorSource % SensorSource.m
methods (Abstract,Static)
reading = latest(sensorId)
end
end
function n = alertOverheated(source,notifier,sensorIds,limit)
% Notify once for each sensor whose latest reading exceeds limit.
n = 0;
for id = sensorIds
if source.latest(id) > limit
notifier.send(id);
n = n + 1;
end
end
end
The test replaces both collaborators. The data source is a stub that returns predefined readings, and the notifier is a spy with a method send that does not exist anywhere yet:
classdef AlertOverheatedTest < matlab.mock.TestCase
% AlertOverheatedTest.m (not run here)
methods (Test)
function notifiesOnlyForSensorsOverTheLimit(testCase)
import matlab.unittest.constraints.IsAnything
import matlab.mock.constraints.WasCalled
% create the mocks and their behavior objects
[stubSource,sourceBehavior] = createMock(testCase,?SensorSource);
[spyNotifier,notifierBehavior] = createMock(testCase, ...
"AddedMethods",{"send"});
% define the stub's behavior
testCase.assignOutputsWhen(sourceBehavior.latest("S1"),301.5);
testCase.assignOutputsWhen(sourceBehavior.latest("S2"),355.0);
testCase.assignOutputsWhen(sourceBehavior.latest("S3"),299.0);
% call the component under test with the mocks
n = alertOverheated(stubSource,spyNotifier,["S1" "S2" "S3"],350);
% qualify the results and the interactions
testCase.verifyEqual(n,1)
testCase.verifyCalled(notifierBehavior.send("S2"))
testCase.verifyNotCalled(notifierBehavior.send("S1"))
testCase.verifyThat(notifierBehavior.send(IsAnything), ...
WasCalled('WithCount',1))
end
end
end
The forms used here follow the documentation’s trader example: createMock(testCase,?DataService) for a stub from a class, createMock(testCase,'AddedMethods',{'buy'}) for a spy with an implicit interface, assignOutputsWhen(behavior.method(args),value) to define a return value, verifyCalled and verifyNotCalled for interactions, and verifyThat(behavior.method(IsAnything),WasCalled('WithCount',2)) to count calls. The count option is written as in the documentation, 'WithCount' followed by the number. By default a mocked method that has no defined behavior returns empty, which is why a pure spy needs no stubbing.
Beyond assignOutputsWhen, the documented building blocks are:
-
actions in
matlab.mock.actions:AssignOutputs,DoNothing,Invoke(run a function handle when the method is called),ReturnStoredValueandStoreValuefor properties, andThrowException(simulate a failing collaborator); -
constraints in
matlab.mock.constraints:WasCalled,WasAccessed,WasSetandOccurred; -
matlab.mock.AnyArgumentsto match any number of arguments, andgetMockHistoryto inspect the interaction history.
Mocks are not magic: a test that mocks everything checks only that the code calls what you told it to call. Use a mock for a boundary that is slow, non-deterministic or not available on the machine that runs the tests (a licence-checked service, a serial port), and use the real object for plain computations, which is why heatflow.laplacian is not mocked anywhere on this page. Tests that combine the app testing framework and mocking are described in the documentation topic linked below.
Performance tests
The performance testing framework measures how long code takes with the same test interfaces: a performance test can be a script, a function or a class, and can contain qualifications that check the result, so it doubles as an ordinary regression test. The documented features are running the code several times to warm it up and accounting for noise. results = runperf(tests) runs a set of tests and returns an array of matlab.perftest.TimeResult objects, one per test. The framework uses a variable number of measurements to reach a sample mean with a 5% relative margin of error at 95% confidence: it runs the test five times to warm up and then between 4 and 256 times to measure, and warns if the margin is not reached after 256 runs. runperf takes the same selection arguments as runtests.
For a class-based performance test, derive from matlab.perftest.TestCase (a handle class that derives from matlab.unittest.TestCase) and use startMeasuring and stopMeasuring to bound the code that is measured, so setup and the qualification are not timed:
classdef LaplacianPerfTest < matlab.perftest.TestCase
% LaplacianPerfTest.m (not run here)
methods (Test)
function laplacianLargeGrid(testCase)
T = rand(2000);
testCase.startMeasuring
L = heatflow.laplacian(T,0.001);
testCase.stopMeasuring
testCase.verifySize(L,[1998 1998])
end
end
end
results = runperf("LaplacianPerfTest");
The returned TimeResult objects carry the measurements; see the reference pages of matlab.unittest.measurement.MeasurementResult and matlab.perftest.TimeResult for how to summarise them. (Not run here, so there are no timings on this page. Timings of one machine are not transferable, and the Octave runs used for this section are not MATLAB timings.) Code that runs too fast for the framework to measure accurately is filtered with an assumption failure, as the documentation’s preallocation example shows ("The MeasuredTime should not be too close to the precision of the framework"). For that case, keepMeasuring loops over the fast code automatically and measures the average. The classes matlab.perftest.TimeExperiment, FixedTimeExperiment and FrequentistTimeExperiment set the measurement strategy, and matlab.unittest.measurement.chart.ComparisonPlot compares two sets of results visually. For quick timing outside a test suite, see tic, toc and timeit on Profiling and Timing.
App tests
Apps built with App Designer, or programmatically with uifigure, are tested with the app testing framework: a test class derives from matlab.uitest.TestCase and uses gesture methods such as press, choose, drag, scroll (R2024a), type and hover on the UI components, and the usual qualifications on the resulting state. The framework is combined with mocking in the documentation topic "Write Tests That Use App Testing and Mocking Frameworks". The heatflow viewer app is described on the App Designer pages of this section.
Pitfalls
-
Teardown far from the change it undoes. A separate teardown block is not tied to a specific change, so it is easy to forget one; register
addTeardownright next to each change, which also makes the cleanup exception-safe. -
Mutating a shared handle in a parameter. One test’s change is visible to the next. Parameterise with value objects or function handles that build a fresh object.
-
Parameters evaluated at class load time that need the file system. Use a
TestParameterDefinitionmethod so that each new suite sees the current files. -
An exhaustive explosion. Three parameters with many values multiply; use
"pairwise"or"sequential"for combinations that need not all run. -
Parameter order in names. In the documented example the name of a test lists the parameters in the order of the method’s arguments; keep it in mind when writing
Namefilters. -
Over-mocking. A mock only checks the calls you expect. Test the integration with the real collaborator in a separate, tagged test.
-
Forgetting that a tolerance belongs to a quantity. The
1e-8in the table above is justified by the printed errors; a different grid or data type needs its own estimate. -
Timing noise in the first run. The first call of a function includes loading and compilation; the performance framework runs warm-up iterations, an ad hoc
tic/tocdoes not. -
Shared fixtures that differ by one argument. Classes are grouped only when their shared fixtures are exactly the same, so a slightly different
PathFixtureargument means two setups.
Where older books differ
The two books consulted for this section do not describe the unit testing framework, so there is no book statement to correct here. The forms on this page are newer than most of the older material you will find online: the performance testing framework (runperf) and the mocking framework, the TestParameter and ParameterCombination attributes, addTeardown in preference to explicit teardown blocks, and name-value syntax with = (R2021a). The app testing framework replaced hand-written scripts that set Value properties and call callbacks, and its scroll gesture is R2024a and doublePress R2026b. For the toolbox-dependent extras (baseline tests, extra coverage metrics, DependsOn selection), the documentation points to MATLAB Test and Simulink Test, which are listed on Toolboxes and Add-On Products; this page uses only base MATLAB.
The same idea in other languages: pytest fixtures, pytest.mark.parametrize and unittest.mock play the roles of MATLAB’s fixtures, parameterized tests and matlab.mock on Python: Unit Testing with pytest & Mocking, and JUnit 5 with Mockito does the same on Java: Testing with JUnit 5 and Mockito.