Tests: Difference between revisions
m (→Test for failure: typo) |
|||
Line 26: | Line 26: | ||
See the file test/fntests.log for additional details. | See the file test/fntests.log for additional details. | ||
The summary indicates that most tests have passed as expected, 3 tests failed that were expected to pass, 6 tests failed that were expected to fail, and 38 tests were skipped due to Octave settings or configuration. | |||
To run tests in a specific file, one can simply specify the path instead of a function name: | To run tests in a specific file, one can simply specify the path instead of a function name: |
Revision as of 17:26, 27 January 2022
Having a thorough test suite is something very important which is usually overlooked. It is an incredible help in preventing regression bugs and quickly assess the status of old code. For example, many packages in Octave Forge become deprecated after losing their maintainer simply because they have no test suite.
GNU Octave has multiple tools that help in creating a comprehensive test suite, accessible to both developers and end-users, as detailed on the Octave manual. Basically, test blocks are %!test
comment blocks, typically at the end of a source file, which are ignored by the Octave interpreter and only read by the test
function.
Running tests
To run all the tests of a specific function, simply use the test
command at the Octave prompt. For example, to run the tests of the Octave function mean
type:
>> test mean PASSES 17 out of 17 tests
These tests are written in the Octave language at the bottom of mean.m
which defines the mean
function. It is important that these tests are also available for the end users so they can test the status of their installation. The whole Octave test suite can be run with:
>> __run_test_suite__ Integrated test scripts: [...] Summary: PASS 11556 FAIL 3 XFAIL 6 SKIPPED 38 See the file test/fntests.log for additional details.
The summary indicates that most tests have passed as expected, 3 tests failed that were expected to pass, 6 tests failed that were expected to fail, and 38 tests were skipped due to Octave settings or configuration.
To run tests in a specific file, one can simply specify the path instead of a function name:
test /full/path/to/file.m
Writing tests
Tests appear as %!
blocks at the bottom of the source file, together with %!demo
blocks. A typical m function file, will have the following structure:
## Copyright
##
## A block with the copyright notice
## -*- texinfo -*-
##
## A block with the help text
function [x, y, z] = foo (bar)
## some amazing code
endfunction
%!assert (foo (1))
%!assert (foo (1:10))
%!assert (foo ("on"), "off")
%!error <must be positive integer> foo (-1)
%!error <must be positive integer> foo (1.5)
%!demo
%! ## see how cool foo() is:
%! foo([1:100])
Tests can be added to oct functions in the C++ sources just as easily, see find.cc for example. The syntax is exactly the same, but done within C comment blocks. During installation, these lines are automatically extracted from the sources and special test scripts are generated. A typical C++ source file has the following structure:
// Copyright
//
// A block with the copyright notice
DEFUN_DLD (foo, args, ,
"-*- texinfo -*-\n\
A block with the help text")
{
// some amazing code
}
/*
%!assert (foo (1))
%!assert (foo (1:10))
%!assert (foo ("on"), "off")
%!error <must be positive integer> foo (-1)
%!error <must be positive integer> foo (1.5)
*/
Assert
%!assert
lines are the simplest one-line tests to write and also
the most common:
%!assert (foo (bar)) # test fails if "foo (bar)" returns false
%!assert (foo (bar), qux) # test fails if "foo (bar)" is different from "qux"
These are actually a shorthand version of
%!test assert (foo (bar))
, and assert
is simply
an Octave function that throws an error when two arguments fail to compare.
Error / Warning
It is also important to test that a function performs its checks correctly
and throws errors (or warnings) when it receives garbage. This can be done with
error
(or warning
) blocks:
%!error foo () # test that causes any error
%!error <BAR must be a positive integer> foo (-1.5) # test that throws specific error message
%!error id=Octave:invalid-fun-call foo () # test that throws specific error id
%!warning foo () # test that causes any warning
%!warning <negative values might give inaccurate results> foo (-1.5) # test that triggers a specific warning message
%!warning id=BAR:possibly-inaccurate-result foo (-1.5) # test that triggers a specific warning id
These are actually shorthand versions of
%!test fail ("foo()", "error message")
and %!test fail ("foo()", "warning", "warning message")
, where %!fail
returns true if the supplied code returns an error or warning.
Test Blocks
While single %!assert
, %!error
, and %!warning
lines are the most common used tests, %!test
blocks offer more features and flexibility. The code within %!test
blocks is simply processed through the Octave interpreter. If the code generates an error, the test is said to fail. Often %!test
blocks end with a call to assert
:
%!test
%! a = [0 1 0 0 3 0 0 5 0 2 1];
%! b = [2 5 8 10 11];
%! for i = 1:5
%! assert (find (a, i), b(1:i))
%! endfor
Test for no failure
In a few cases, there is the situation where a function returns nothing, and the only thing to test is that it causes no error. This can be tested simply with:
%!test foo (bar)
Test for failure
If a warning or error message cannot be tested with one of the single-line tests mentioned above, the fail
function can be used within a test block to verify expected error and warning functionality such as:
%!test
%! a = [1 2 3];
%! b = [1 2];
%! fail ("a + b", "nonconformant arguments")
%!test
%! a = 111;
%! b = 112;
%! fail ("['foo', a, b]", "warning", "implicit conversion from numeric to char")
The tests above pass if the errors/warnings occur as expected.
It is often useful to share a function among multiple tests. Sometimes
these are only small helper functions, but more often these are just simpler
low performance implementations of the function being tested. These are
created in %!function
blocks:
%!function x = slow_foo (bar)
%! ## a simple implementation of foo, definitely correct, but
%! ## unfortunately too slow for anything other than tests.
%!endfunction
%!assert (foo (bar), slow_foo (bar))
%!test
%! for i = -100:100
%! bar = qux (i);
%! assert (foo (bar), slow_foo (bar))
%! endfor