Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Examples driven tester is a tool for users which uses MATLAB® scripts which
* **TestReportFormat** - Format of test report. ***Possible values:** "pdf", "docx" , ["html”], “xml”*
* **OutputPath** - Directory where reports will be generated. Default is pwd.
* **CodeCoveragePlugin** - [MATLAB Code Coverage plugin](https://www.mathworks.com/help/matlab/ref/matlab.unittest.plugins.codecoverageplugin-class.html).
* **CleanupFcn** - Function handle executed after each test method for custom cleanup (e.g., closing Simulink models). Default is empty.

***Note:** Values enclosed in square braces are default values.*

Expand Down Expand Up @@ -67,6 +68,25 @@ covPlugin = matlab.unittest.plugins.CodeCoveragePlugin.forFolder("code", "Produc
obj = examplesTester(["examples", "doc"], CodeCoveragePlugin = covPlugin);
obj.executeTests;
```

Run MATLAB scripts from specified folders and close all Simulink models after each test.

```matlab
obj = examplesTester(["examples", "doc"], CleanupFcn = @() bdclose('all'));
obj.executeTests;
```

Run MATLAB scripts with a custom cleanup function that performs multiple cleanup steps.

```matlab
obj = examplesTester(["examples", "doc"], CleanupFcn = @myCleanup);
obj.executeTests;

function myCleanup()
bdclose('all'); % Close Simulink models
Simulink.sdi.clear; % Clear Signal Data Inspector
end
```
## Integration with MATLAB's BuildTool
From MATLAB R2025a and onwards, users can use the `ExampleDrivenTesterTask`, a ready-to-use buildtool task shipped with ExamplesDrivenTester for automated example testing.

Expand Down Expand Up @@ -97,6 +117,11 @@ covPlugin = matlab.unittest.plugins.CodeCoveragePlugin.forFolder("code", "Produc
plan("runExample") = ExampleDrivenTesterTask(["examples", "doc"], CodeCoveragePlugin = covPlugin);
```

5. Run MATLAB scripts with a custom cleanup function (e.g., close Simulink models after each test):
```matlab
plan("runExample") = ExampleDrivenTesterTask(["examples", "doc"], CleanupFcn = @() bdclose('all'));
```

## License

The license is available in the [LICENSE.txt](license.txt) file within this repository
Expand Down
37 changes: 37 additions & 0 deletions tests/tExamplesTester.m
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,43 @@ function nonStringTestFolders(testCase)
expectedError = "examplesTester:NonStringTestFolder";
testCase.verifyError(@()examplesTester(23), expectedError);
end

function verifyCleanupFcnIsCalled(testCase)
% Test verifies that a user-provided CleanupFcn is called after
% each test method execution

import matlab.unittest.fixtures.TemporaryFolderFixture
testCase.applyFixture(TemporaryFolderFixture);

cleanupCallCount = 0;
function incrementCount()
cleanupCallCount = cleanupCallCount + 1;
end

obj = examplesTester("examples", ...
CleanupFcn=@incrementCount);
obj.OutputPath = testCase.createTemporaryFolder;
obj.executeTests();

testCase.verifyGreaterThan(cleanupCallCount, 0, ...
"CleanupFcn was not called during test execution");
testCase.verifyTrue(all([obj.TestResults.Passed]), 'All tests did not pass');
end

function verifyCleanupFcnDefaultIsEmpty(testCase)
% Test verifies that CleanupFcn defaults to empty
obj = examplesTester("examples");
testCase.verifyEmpty(obj.CleanupFcn, ...
"Default value of CleanupFcn should be empty");
end

function verifyInvalidCleanupFcn(testCase)
% Verify appropriate error is thrown when CleanupFcn is not a
% function handle
expectedError = "examplesTester:InvalidCleanupFcn";
testCase.verifyError(@()examplesTester("examples", CleanupFcn="notAFunction"), ...
expectedError);
end
end

end
16 changes: 16 additions & 0 deletions toolbox/examplesTester.m
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s a case‑sensitivity mismatch here: the switch checks for 'Docx', while user input is "docx".
Validation passes, but the switch never matches and ends up in otherwise.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
% TestReportFormat - Format of test report. Possible values: "pdf", "docx" , ["html”], “xml”
% OutputPath - Directory where reports will be generated. Default is "test-report"
% CodeCoveragePlugin - MATLAB CodeCoverage plugin. Default value is empty
% CleanupFcn - Function handle executed after each test method. Default is empty
%
% Description
% ------------
Expand All @@ -28,6 +29,8 @@
%
% obj = examplesTester(["test", "doc"], TestReportFormat = "pdf"); creates a code coverage report in PDF format
%
% obj = examplesTester(["test", "doc"], CleanupFcn = @() bdclose('all')); closes all Simulink models after each test
%
% Copyright 2023 The MathWorks, Inc.

properties
Expand All @@ -37,6 +40,7 @@
TestFolders (1, :) {examplesTester.validateTestFolders(TestFolders)} = pwd
TestResults
CodeCoveragePlugin {examplesTester.validateCodeCoveragePlugin} = []
CleanupFcn {examplesTester.validateCleanupFcn} = []
end

properties (Access=private)
Expand All @@ -58,12 +62,14 @@
args.TestReportFormat = "html"
args.OutputPath = pwd
args.CodeCoveragePlugin = []
args.CleanupFcn = []
end

obj.CreateTestReport = args.CreateTestReport;
obj.TestReportFormat = args.TestReportFormat;
obj.OutputPath = args.OutputPath;
obj.CodeCoveragePlugin = args.CodeCoveragePlugin;
obj.CleanupFcn = args.CleanupFcn;

if examplesTester.isJsonPath(TestFolders)
obj.readTestFiles(TestFolders);
Expand Down Expand Up @@ -94,6 +100,8 @@ function executeTests(obj)
testFilesAndFolders = Parameter.fromData('tests', obj.testFiles);
suite = TestSuite.fromPackage(obj.testPackage, 'ExternalParameters', testFilesAndFolders);

setappdata(0, 'ExamplesTester_CleanupFcn', obj.CleanupFcn);
cleanupObj = onCleanup(@() rmappdata(0, 'ExamplesTester_CleanupFcn'));
obj.TestResults = obj.Runner.run(suite);

end
Expand Down Expand Up @@ -302,6 +310,14 @@ function validateCodeCoveragePlugin(codeCoveragePlugin)
error("Invalid value for CodeCoveragePlugin");
end
end

function validateCleanupFcn(cleanupFcn)
% Method validates the value of CleanupFcn property
if ~isempty(cleanupFcn) && ~isa(cleanupFcn, 'function_handle')
error("examplesTester:InvalidCleanupFcn", ...
"CleanupFcn must be a function handle or empty");
end
end
end

% Get set methods
Expand Down
7 changes: 7 additions & 0 deletions toolbox/internal/+tests/wrapperTest.m
Copy link
Copy Markdown
Contributor

@vanditaMW vanditaMW Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be mistaken, but I believe there is no guaranteed execution order between multiple TestMethodTeardown methods in matlab.unittest. In that case, if runCustomCleanup throws an error, closeAllFigures may not run (or vice versa).
The MATLAB doc recommend registering teardown logic using addTeardown instead of multiple TestMethodTeardown methods, since addTeardown provides a defined LIFO order and is exception‑safe.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ function closeAllFigures(~)
figHandles = findall(groot,'Type','figure');
close(figHandles, "force");
end

function runCustomCleanup(~)
cleanupFcn = getappdata(0, 'ExamplesTester_CleanupFcn');
if ~isempty(cleanupFcn)
cleanupFcn();
end
end
end

methods (Test)
Expand Down
10 changes: 8 additions & 2 deletions toolbox/internal/ExampleDrivenTesterTask.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
% - TestReportFormat (string)
% - ReportOutputFolder (string)
% - CodeCoveragePlugin (object)
% - CleanupFcn (function_handle) - Custom cleanup function executed after each test

properties
Folders (1,:) string
CreateTestReport (1,1) logical
TestReportFormat (1,1) string
OutputPath (1,1) string
CodeCoveragePlugin
CleanupFcn
end

methods
Expand All @@ -25,6 +27,7 @@
options.TestReportFormat (1,1) string {mustBeMember(options.TestReportFormat,["html", "pdf", "docx", "xml"])} = "html"
options.OutputPath(1,1) string = "reports_" + char(datetime('now', 'Format', 'yyyyMMdd_HHmmss'))
options.CodeCoveragePlugin = []
options.CleanupFcn = []
end

task.Description = "Run published examples";
Expand All @@ -44,6 +47,7 @@
task.TestReportFormat = options.TestReportFormat;
task.OutputPath= options.OutputPath;
task.CodeCoveragePlugin= options.CodeCoveragePlugin;
task.CleanupFcn = options.CleanupFcn;

if task.CreateTestReport
task.Outputs = task.OutputPath;
Expand All @@ -65,15 +69,17 @@ function runExampleTests(task, ~)
task.Folders, ...
CreateTestReport = task.CreateTestReport, ...
TestReportFormat = task.TestReportFormat, ...
OutputPath = task.OutputPath);
OutputPath = task.OutputPath, ...
CleanupFcn = task.CleanupFcn);
else
% Pass CodeCoveragePlugin through when provided
examplesRunner = examplesTester( ...
task.Folders, ...
CreateTestReport = task.CreateTestReport, ...
TestReportFormat = task.TestReportFormat, ...
OutputPath = task.OutputPath, ...
CodeCoveragePlugin = task.CodeCoveragePlugin);
CodeCoveragePlugin = task.CodeCoveragePlugin, ...
CleanupFcn = task.CleanupFcn);
end
examplesRunner.executeTests;
end
Expand Down