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:

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


  1. 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 the PATH environment variable.

  2. 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.