Skip to content

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 and data 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.