Fortran Unit Testing with fUnit
December 6, 2008
fUnit is a Fortran unit testing framework developed by engineers at NASA. It requires a Fortran 95 compiler and it is designed for testing routines contained in modules. fUnit is written in Ruby and is distributed as a Ruby Gem at RubyForge.
Installing fUnit
If you have rubygems
installed, simply running
sudo gem install funit
will install fUnit.1 You must also have the FC
environment
variable set to your Fortran compiler:
export FC="gfortran"
Tests and Assertions
Tests are simple Fortran fragments stored in a .fun
file of the same
name as the module. Each unit test file can contain multiple tests
as well as setup and teardown commands that are executed once for
each test. Tests may make six types of assertions:
assert_true(expression)
assert_false(expression)
assert_equal(a, b)
assert_real_equal(a, b)
assert_equal_with(a, b, tol)
assert_array_equal(a,b)
2
An Example Class
As an example, we will write a simple module called circle_class
.
The class will be stored in circle_class.f90
and the unit tests in
circle_class.fun
. Both files must reside in the same directory.
Here is circle_class.f90
:
module circle_class
implicit none
private
public :: circle, circle_area
real :: pi = 4.d0 * atan(1.d0)
type circle
real :: radius
end type circle
contains
function circle_area(this) result(area)
type(circle), intent(in) :: this
real :: area
area = pi * this%radius**2
end function circle_area
end module circle_class
An Example Test Suite
Now, we will write a simple unit test file
circle_class.fun
which illustrates all six
assertions as well as the setup
and teardown
routines:
test_suite circle_class
! Global variables can be declared here
real, parameter :: radius = 1.5d0
real, parameter :: pi = 3.14159d0
type(circle) :: c
setup
! Place code here that should run before each test
c = circle(radius)
end setup
teardown
! This code runs immediately after each test
end teardown
! Example test using all six assertions
test funit_assertions
integer, dimension(2) :: a = (/ 1, 2 /)
integer, dimension(2) :: b = (/ 1, 2 /)
assert_array_equal(a,b)
assert_real_equal(0.9999999e0, 1.0e0)
assert_equal_within(1e-7, 0.0, 1e-6)
assert_equal(1, 5 - 4)
assert_false(5 < 4)
assert_true(4 == 4)
end test
test radius_is_stored_properly
assert_real_equal(radius, 1.5d0)
end test
test area_varies_with_radius
real :: area
area = circle_area(c)
assert_equal_within(area, pi*(radius**2), 1e-3)
end test
end test_suite
Testing the Example
Runing these tests is just a matter of running funit
with the module
name:
% funit circle_class
expanding test suite: circle_class...done.
computing dependencies
locating associated source files and sorting for compilation
(cd .; gfortran -c circle_class_fun.f90)
(cd .; gfortran -c TestRunner.f90)
gfortran -o TestRunner circle_class.o circle_class_fun.o TestRunner.o
circle_class test suite:
Passed 8 of 8 possible asserts comprising 3 of 3 tests.
==========[ SUMMARY ]==========
circle_class: passed
Errors
Note that we have used different approximations for π in the module and
test suite: –4 arctan(–1) and 1.14159. To see what happens when a test
fails, suppose we change the tolerance in the assert_equal_within
assertion from 1e-3
to 1e-6
:
% funit circle_class
expanding test suite: circle_class...done.
computing dependencies
locating associated source files and sorting for compilation
make[1]: Entering directory `/tmp/funit'
(cd .; gfortran -c circle_class.f90)
(cd .; gfortran -c circle_class_fun.f90)
(cd .; gfortran -c TestRunner.f90)
gfortran -o TestRunner circle_class.o circle_class_fun.o TestRunner.o
make[1]: Leaving directory `/tmp/funit'
circle_class test suite:
*Assert_Equal_Within failed* in test area_varies_with_radius [circle_class.fun:37]
area ( 7.0685835 ) is not 7.0685778 within 9.99999997E-07
Passed 7 of 8 possible asserts comprising 2 of 3 tests.
==========[ SUMMARY ]==========
circle_class: failed <<<<<
STOP 1
fUnit tells us which test failed (area_varies_with_radius
), which
assertion failed (Assert_Equal_Within
), and which line the offending
assertion is on (37).
Cleanup
Finally, although fUnit creates several temporary files
(circle_class_fun.f90
, TestRunner.90
, etc.) it will clean up
after itself if you run funit --clean
.
An Example Makefile
Here is a simple Makefile for carrying out the tests and cleaning up:
test:
funit circle_class
clean:
-rm *.o *.mod
funit --clean
Notes
Test files must be stored in the same directory as the corresponding source file.
Test files must have the same name as the module being tested, thus
circle_class.f90
contains the modulecircle_class
which has unit tests incircle_class.fun
.The tests have to be aligned at the first column–if they are indented you will get an error similar to the following:
*Error: assert_real_equal assertion not in a test block
-
Depending on your system, you may need to add the gem location to your path. For example, on Debian GNU/Linux systems gems are installed in
/var/lib/gems/1.8
and one must add/var/lib/gems/1.8/bin
to thePATH
environment variable. ↩ -
The lack of whitespace here is intentional. I’ve submitted a patch to the fUnit developers to allow whitespace after the comma here, but until it’s applied anything but
(a,b)
will cause a syntax error. ↩