Testing#
graph LR
A[input] --> B(function)
B --> C[output]
- create a function
function c = add_numbers(a, b)
% Add 2 numbers.
% (C) Copyright 2023 Remi Gau
if ~isnumeric(a)
err.message = 'a must a scalar value.';
err.identifier = 'add_numbers:ScalarExpected';
error(err);
end
c = a + b;
end
Using assert
#
graph LR
A[input] --> B(function)
B --> C[output]
C --> D(assert)
- use
assert
inside the function to check some aspect of the behavior of the function
function c = add_numbers_with_assert(a, b)
% (C) Copyright 2023 Remi Gau
c = a + b;
assert(numel(c) == 1, 'b must be a scalar');
end
Smoke test#
flowchart LR
subgraph test
direction LR
A[some_input] --> B(function)
B --> C[some_output]
end
- create a "smoke" test by writing a function that:
- calls the function with some input
function test_add_numbers_smoke()
% (C) Copyright 2023 Remi Gau
a = 1;
b = 2;
c = add_numbers(a, b);
end
Unit test#
flowchart LR
subgraph test
direction LR
A[known_input] --> B(function)
B --> C[output]
C --> E(assertEqual)
D[expected_output] --> E
end
- create a unit test by writing a function that:
- calls the function with a specific input
- asserts that that the output is as expected
function test_add_numbers_unit()
% (C) Copyright 2023 Remi Gau
a = 1;
b = 2;
c = add_numbers(a, b);
assert(c == 3);
end
- test the function with a variety of inputs
Other example#
plot_line
#
function handle = plot_line(x, y)
% Create a PNG file for the line for values x and y.
%
% (C) Copyright 2023 Remi Gau
handle = figure('name', 'line');
plot(x, y);
xlabel('x values');
ylabel('y values');
print(gcf, 'my_figure.png', '-dpng');
end
function test_plot_line()
% (C) Copyright 2023 Remi Gau
% set up
% make sure we start from a "clean slate"
if exist('my_figure.png', 'file')
delete('my_figure.png');
end
close all;
x = 1:100;
y = 3 * x + 5 + randn(1, 100) * 10;
handle = plot_line(x, y);
assert(strcmp(handle.Name, 'line'));
assert(exist('my_figure.png', 'file') == 2);
% teardown
% remove an file created during the test
delete('my_figure.png');
close all;
end
Using a testing framework#
- rewrite the unit test so that it can be run with MOxUnit or MATLAB testing framework
With MoxUnit#
function test_suite = test_add_numbers_moxunit %#ok<STOUT>
% (C) Copyright 2023 Remi Gau
try % assignment of 'localfunctions' is necessary in Matlab >= 2016
test_functions = localfunctions(); %#ok<NASGU>
catch % no problem; early Matlab versions can use initTestSuite fine
end
initTestSuite;
end
function test_add_numbers_basic()
a = 1;
b = 2;
c = add_numbers(a, b);
assertEqual(c, 3);
end
success = moxunit_runtests(test_folder, ...
'-verbose', ...
'-recursive', ...
'-cover', source_cover)
Other example#
create_participant_file
#
function filename = create_participant_file(subject_nb, task_name, run_nb)
% Create a partcipant CSV filename for a subject, task, run
%
% (C) Copyright 2023 Remi Gau
% use assert for input parameter validation
assert(isnumeric(run_nb), 'run number should be a number');
% convert any eventual numeric value into a char
if isnumeric(subject_nb)
subject_nb = num2str(subject_nb);
end
filename = ['sub-' subject_nb, ...
'_task-' task_name, ...
'_run-' num2str(run_nb) '.csv'];
end
function test_suite = test_create_participant_file %#ok<STOUT>
% (C) Copyright 2023 Remi Gau
try % assignment of 'localfunctions' is necessary in Matlab >= 2016
test_functions = localfunctions(); %#ok<NASGU>
catch % no problem; early Matlab versions can use initTestSuite fine
end
initTestSuite;
end
function test_smoke
run_numbers = -10:0:10;
for i = 1:numel(run_numbers)
create_participant_file('01', 'rest', run_numbers(i));
end
end
function test_unit_one
filename = create_participant_file('01', 'rest', 1);
expected_output = 'sub-01_task-rest_run-1.csv';
assert(ischar(filename));
assert(strcmp(filename, expected_output));
end
function test_unit_two
filename = create_participant_file('02', 'rest', 1);
expected_output = 'sub-02_task-rest_run-1.csv';
assert(ischar(filename));
assert(strcmp(filename, expected_output));
end
function test_unit_three
filename = create_participant_file('02', 'rest', 2);
expected_output = 'sub-02_task-rest_run-2.csv';
assert(ischar(filename));
assert(strcmp(filename, expected_output));
end
function test_unit_five
filename = create_participant_file('02', 'rest', 1);
expected_output = 'sub-02_task-rest_run-1.csv';
assert(ischar(filename));
assert(strcmp(filename, expected_output));
end
function test_unit_subject_number_as_number
filename = create_participant_file(2, 'rest', 2);
expected_output = 'sub-2_task-rest_run-2.csv';
assert(strcmp(filename, expected_output));
end
With MATLAB#
function tests = test_add_numbers_matlab
% (C) Copyright 2023 Remi Gau
tests = functiontests(localfunctions);
end
function test_add_numbers_basic(testCase) %#ok<*INUSD>
a = 1;
b = 2;
c = add_numbers(a, b);
assert(c == 3);
end
Code coverage#
- write a script to use MoxUnit to run all the tests and generate a code coverage report
% (C) Copyright 2023 Remi Gau
%
% Script to:
% - run all the moxunit tests on the analysis code
% - generate an coverage HTM and XML report
% - create a log file to report if any test failed (used in CI)
folderToCover = fullfile(pwd, 'src');
testFolder = fullfile(pwd, 'tests');
success = moxunit_runtests(testFolder, ...
'-verbose', ...
'-recursive', ...
'-with_coverage', ...
'-cover', folderToCover, ...
'-cover_xml_file', 'coverage.xml', ...
'-cover_html_dir', fullfile(pwd, 'coverage_html'));
if success
system('echo 0 > test_report.log');
else
system('echo 1 > test_report.log');
end
Testing "legacy" code#
- create a new repository and add the
code
anddata
folder in it - add tests that should check that the functions in
code
:- create figures
- save data that is "equivalent" to the one already present in the
data
folder,
function test_suite = test_analyse %#ok<STOUT>
% (C) Copyright 2023 Remi Gau developers
try % assignment of 'localfunctions' is necessary in Matlab >= 2016
test_functions = localfunctions(); %#ok<NASGU>
catch % no problem; early Matlab versions can use initTestSuite fine
end
initTestSuite;
end
function test_analyse_basic()
%% set up
root_dir = fullfile(fileparts(mfilename('fullpath')), '..');
data_dir = fullfile(root_dir, 'data');
subject_dir = fullfile(data_dir, 'sub-01');
output_figure = fullfile(subject_dir, ...
'Behavioral', ....
'Figures.ps');
output_mat_file = fullfile(subject_dir, ...
'Behavioral', ...
'Results_PIEMSI_1.mat');
if exist(output_mat_file, 'file')
delete(output_mat_file);
end
if exist(output_figure, 'file')
delete(output_figure);
end
close all;
%% test
cd(subject_dir);
Analyse();
% check created files
assert(exist(output_figure, 'file') == 2);
assert(exist(output_mat_file, 'file') == 2);
% check all values are those exepected
expected = load(fullfile(subject_dir, ...
'Behavioral', ...
'expected_results.mat'));
results = load(fullfile(subject_dir, ...
'Behavioral', ...
'Results_PIEMSI_1.mat'));
assertEqual(results, expected);
%% tear down
% remove any files created during the test
delete(output_figure);
delete(fullfile(subject_dir, 'Behavioral', 'Fig*.eps'));
close all;
end
- get a code coverage report for the tests
% (C) Copyright 2023 Remi Gau
%
% Script to:
% - run all the mowunit tests
% - generate an coverage HTM and XML report
% - create a log file to report if any test failed (used in CI)
folderToCover = fullfile(pwd, 'code');
testFolder = fullfile(pwd, 'tests');
success = moxunit_runtests(testFolder, ...
'-verbose', '-recursive', '-with_coverage', ...
'-cover', folderToCover, ...
'-cover_xml_file', 'coverage.xml', ...
'-cover_html_dir', fullfile(pwd, 'coverage_html'));
if success
system('echo 0 > test_report.log');
else
system('echo 1 > test_report.log');
end
References#
See the references page for more information.