Key concepts#
Fortuno is built around the following key concepts:
Test cases (often referred as tests): Represent individual named unit tests and contain the code to execute, when the test is run.
Test suites: Represent named test containers for structuring your tests. They might contain test cases and further test suites (up to arbitrary nesting level). Their initialization (set-up) and finalization (tear-down) is customizable and they might provide data for the test cases and test suites they contain.
Test apps: Driver programs responsible for setting up and tearing down the test suites and running the tests.
Depending, whether the routines you test are serial (eventually with OpenMP-parallelization), MPI-parallelized or coarray-parallelized, you need to use different versions of these objects. Fortuno offers for all these three cases a special interface.
Additionally, Fortuno uses an unnamed container for tests and tests suites:
Test lists: Collect various test cases and test suites.
Analyzing a minimal working example#
Let’s use the example from the Tutorials section to understand, how some of those key concepts can be combined to build unit tests (and to learn some best practices with Fortuno):
!> Fortuno unit tests
module test_mylib
use mylib, only : factorial
use fortuno_serial, only : is_equal, test => serial_case_item, check => serial_check, test_list
implicit none
contains
function tests()
type(test_list) :: tests
tests = test_list([&
test("factorial_0", test_factorial_0),&
test("factorial_1", test_factorial_1),&
test("factorial_2", test_factorial_2)&
])
end function tests
! Test: 0! = 1
subroutine test_factorial_0()
call check(factorial(0) == 1)
end subroutine test_factorial_0
! Test: 1! = 1
subroutine test_factorial_1()
call check(is_equal(factorial(1), 1))
end subroutine test_factorial_1
! Test: 2! = 3 (will fail to demonstrate the output of a failing test)
subroutine test_factorial_2()
! Failing check, you should obtain detailed info about the failure.
call check(&
& is_equal(factorial(2), 3),&
& msg="Test failed for demonstration purposes"&
)
end subroutine test_factorial_2
end module test_mylib
!> Test app driving Fortuno unit tests.
program testapp
use test_mylib, only : tests
use fortuno_serial, only : execute_serial_cmd_app
implicit none
call execute_serial_cmd_app(tests())
end program testapp
So what happened here?
First of all, we defined a module for the tests and a program which drives the testing. It is
generally a good practice to separate the unit tests from the driver program as one driver program
might be reponsible for driving tests from multiple modules. The test module contains the function
tests(), which returns the list of the unit tests it wants to expose.
In the test module, we have imported following objects:
use fortuno_serial, only : is_equal, test => serial_case_item, check => serial_check, test_list
is_equal: Function to check the equality of two objects returning detailed information about the check.serial_case_item: Function returing a wrapped test case object for serial tests. The_itemsuffix indicates that this is a wrapper allowing to use the test case object as an item (an element) of an array. We have introduced the abbreviationtestfor this rather longish name.serial_check: Subroutine for registering the result of an actual check in serial tests, abbreviated here ascheck.test_list: Type to use for collecting the tests.
The function tests() is pretty simple, we just return a test_list instance containing all
the tests we wish to export from this module.
function tests()
type(test_list) :: tests
tests = test_list([&
test("factorial_0", test_factorial_0),&
test("factorial_1", test_factorial_1),&
test("factorial_2", test_factorial_2)&
])
end function tests
For creating the individual test items, we employed the serial_test_case_item() function (using
its local abbreviated name test()). In each invocation, we provided a distinctive name for the
test and specified the subroutine that should be executed when the test is run.
Then the unit tests were defined in form of subroutines:
! Test: 0! = 1
subroutine test_factorial_0()
call check(factorial(0) == 1)
end subroutine test_factorial_0
! Test: 1! = 1
subroutine test_factorial_1()
call check(is_equal(factorial(1), 1))
end subroutine test_factorial_1
! Test: 2! = 3 (will fail to demonstrate the output of a failing test)
subroutine test_factorial_2()
! Failing check, you should obtain detailed info about the failure.
call check(&
& is_equal(factorial(2), 3),&
& msg="Test failed for demonstration purposes"&
)
end subroutine test_factorial_2
Our (rather simple) test subroutines need no arguments, they interact with the testing framework by
calling specific subroutines, such as check() in our example. The check() subroutine accepts
either a logical expression—for instance, factorial(0) == 1—or a unique type, as returned by the
is_equal() function, which encapsulates the outcome of the comparison and additional details in
case of a failure. The check() call registers the verification outcome in the framework,
including any failure specifics. A test is deemed successful if no check() calls with failing
(e.g. logically false) argument had been triggered during the run.
The actual program driver program is trivial, we just executed the serial command line app with all the tests we have written.
!> Test app driving Fortuno unit tests.
program testapp
use test_mylib, only : tests
use fortuno_serial, only : execute_serial_cmd_app
implicit none
call execute_serial_cmd_app(tests())
end program testapp
We utilized the execute_serial_cmd_app() subroutine, feeding it with the list of test items
returned by the tests() function of the test module. You shouldn’t add any code after this call,
as it would not return. Once execute_serial_cmd_app() completes its task, it halts the code and
communicates the result to the operating system via an exit code—0 if all tests pass, or a positive
integer to indicate failures.