Test suite fixtures#

You will learn to

  • create test suite fixtures with Fortuno.

Note

This section assumes, that you already have a working project with Fortuno unit tests. You can set up one by using the Fortran project cookiecutter template or following the instructions in the Tutorials section.

Sometimes initialization and finalization must not be executed for each test separately, but only once, before any tests from a group of tests are run or after all tests of the group had been carried out. A typical scenario would be, if you have to load or generate large amount of data before a certain group of tests accessing that data can be executed.

Those cases can be handled in Fortuno using suite fixtures. Test suites in Fortuno have basically two functions:

  • They serve as containers for building hierarchical test inftrastructure, each suite containing tests and further test suites.

  • They can be extended to contain customized data and customized set-up and tear-down procedures. The set-up procedure of a suite is warranted to be executed before any tests in the suite are run, or before the set-up procedure of any of the contained suites is invoked. The tear-down procedure is warranted to be executed after all tests in the test suite had been carried out and the tear-down procedures of all contained suites had been invoked.

For demonstration purposes we will modify the example from the previous section: We will open a file before a group of test is executed, and close it, once the tests had been carried out. The information about the name of the file and the file unit should be passed to the tests. As demonstrated below, there are two ways of passing data to individual tests of a suite: either by using global module variables or using type components introduced by type extension. Former is considerably simpler, but prone to accidental errors, especially when using with complex test hierarchies. Latter needs somewhat more coding, but is is modular and robust.

Storing suite data in module variables#

test/testapp.f90#
  1!> Module containing unit tests
  2module test_mylib
  3  use random_file, only : random_file_name
  4  use fortuno_serial, only : check => serial_check, check_failed => serial_check_failed,&
  5      & serial_suite_base, test => serial_case_item, test_item, test_list
  6  implicit none
  7
  8  type :: test_env
  9    character(:), allocatable :: filename
 10    integer :: unit = -1
 11  contains
 12    final :: final_test_env
 13  end type test_env
 14
 15
 16  ! Fixtured test suite
 17  type, extends(serial_suite_base) :: tempfile_test_suite
 18  contains
 19    procedure :: set_up => tempfile_test_suite_set_up
 20    procedure :: tear_down => tempfile_test_suite_tear_down
 21  end type tempfile_test_suite
 22
 23
 24  ! The global test environment instance.
 25  type(test_env), allocatable :: global_env
 26
 27contains
 28
 29  !> Returns the list of tests in this module
 30  function tests()
 31    type(test_list) :: tests
 32
 33    tests = test_list([&
 34        tempfile_suite("tempfile_demo", test_list([&
 35            test("tempfile_1", test_tempfile_1),&
 36            test("tempfile_2", test_tempfile_2)&
 37        ]))&
 38    ])
 39
 40  end function tests
 41
 42
 43  !> Intializes the test environment (opens temporary file)
 44  !!
 45  !! Note: A very simple-minded implementation, for demonstration purposes only.
 46  !!
 47  subroutine init_test_env(this)
 48
 49    !> Test environment containing file name and file unit.
 50    !!
 51    !! Note: if the opening of the temporary file fails for any reasons the unit remains at its
 52    !! default value (-1) and the file name string will be unallocated.
 53    type(test_env), intent(out) :: this
 54
 55    integer :: iostat
 56
 57    this%filename = random_file_name("tmp-", ".txt", 10)
 58    !open(newunit=this%unit, file=this%filename, action="readwrite", iostat=iostat)
 59
 60  end subroutine init_test_env
 61
 62
 63  !> Finalizes the test environment (closes temporary file)
 64  subroutine final_test_env(this)
 65    type(test_env), intent(inout) :: this
 66    
 67    if (this%unit /= -1) then
 68      ! You might want to add status="delete" to remove the temporary file
 69      close(this%unit)
 70    end if
 71
 72  end subroutine final_test_env
 73
 74
 75  !> Returns a tempfile_suite instance wrapped as test_item to be used in an array constructors.
 76  function tempfile_suite(name, tests) result(testitem)
 77    character(*), intent(in) :: name
 78    type(test_list), intent(in) :: tests
 79    type(test_item) :: testitem
 80
 81    testitem = test_item(tempfile_test_suite(name=name, tests=tests))
 82
 83  end function tempfile_suite
 84
 85  
 86  !> Initializes the test suite.
 87  subroutine tempfile_test_suite_set_up(this)
 88    class(tempfile_test_suite), intent(inout) :: this
 89
 90    allocate(global_env)
 91    call init_test_env(global_env)
 92    call check(global_env%unit /= -1, msg="Failed to open tempfile")
 93    if (check_failed()) deallocate(global_env)
 94
 95  end subroutine tempfile_test_suite_set_up
 96
 97
 98  !> Finalizes the test suite.
 99  subroutine tempfile_test_suite_tear_down(this)
100    class(tempfile_test_suite), intent(inout) :: this
101
102    if (allocated(global_env)) deallocate(global_env)
103
104  end subroutine tempfile_test_suite_tear_down
105  
106
107  subroutine test_tempfile_1()
108    write(global_env%unit, "(a)") "Hello from test_tempfile_1"
109  end subroutine test_tempfile_1
110
111  
112  subroutine test_tempfile_2()
113    write(global_env%unit, "(a)") "Hello from test_tempfile_2"
114  end subroutine test_tempfile_2
115
116end module test_mylib
117
118
119!> Test app driving Fortuno unit tests.
120program testapp
121  use test_mylib, only : tests
122  use fortuno_serial, only : execute_serial_cmd_app
123  implicit none
124
125  call execute_serial_cmd_app(tests())
126
127end program testapp

Storing data within test suites#

test/testapp.f90#
  1!> Module containing unit tests
  2module test_mylib
  3  use random_file, only : random_file_name
  4  use fortuno_serial, only : serial_case_base, check => serial_check,&
  5      & check_failed => serial_check_failed, scope_pointers => serial_scope_pointers,&
  6      & serial_suite_base, test_item, test_list, test_ptr_item
  7  implicit none
  8
  9  
 10  !> Environment representing the test fixture.
 11  type :: test_env
 12    character(:), allocatable :: filename
 13    integer :: unit = -1
 14  contains
 15    final :: final_test_env
 16  end type test_env
 17
 18  
 19  !> Extended test suite containing customized data, initializer and finalizer.
 20  type, extends(serial_suite_base) :: tempfile_test_suite
 21    type(test_env), allocatable :: env
 22  contains
 23    procedure :: set_up => tempfile_test_suite_set_up
 24    procedure :: tear_down => tempfile_test_suite_tear_down
 25  end type tempfile_test_suite
 26
 27  
 28  !> Extended test case running a test procedure with one argument
 29  type, extends(serial_case_base) :: tempfile_test_case
 30    procedure(test_tempfile_1), pointer, nopass :: proc
 31  contains
 32    procedure :: run => tempfile_test_case_run
 33  end type tempfile_test_case
 34
 35contains
 36
 37  !!
 38  !! Tests
 39  !! 
 40
 41  !> Returns the list of tests exported from this module.
 42  function tests()
 43    type(test_list) :: tests
 44
 45    tests = test_list([&
 46        tempfile_suite("tempfile_demo", test_list([&
 47            tempfile_test("tempfile_1", test_tempfile_1),&
 48            tempfile_test("tempfile_2", test_tempfile_2)&
 49        ]))&
 50    ])
 51
 52  end function tests
 53
 54  
 55  !> Test 1
 56  subroutine test_tempfile_1(env)
 57    type(test_env), intent(in) :: env
 58    write(env%unit, "(a)") "Hello from test_tempfile_1"
 59  end subroutine test_tempfile_1
 60
 61  
 62  !> Test 2
 63  subroutine test_tempfile_2(env)
 64    type(test_env), intent(in) :: env
 65    write(env%unit, "(a)") "Hello from test_tempfile_2"
 66  end subroutine test_tempfile_2
 67
 68  
 69  !!
 70  !! Fixture infrastructure
 71  !!
 72
 73  !> Intializes the test environment (opens temporary file).
 74  subroutine init_test_env(this)
 75    type(test_env), intent(out) :: this
 76
 77    integer :: iostat
 78
 79    this%filename = random_file_name("tmp-", ".txt", 10)
 80    open(newunit=this%unit, file=this%filename, action="readwrite", iostat=iostat)
 81
 82  end subroutine init_test_env
 83
 84
 85  !> Finalizes the test environment (closes temporary file).
 86  subroutine final_test_env(this)
 87    type(test_env), intent(inout) :: this
 88    
 89    if (this%unit /= -1) then
 90      close(this%unit)
 91    end if
 92
 93  end subroutine final_test_env
 94
 95
 96  !> Wraps a tempfile_test_suite instance as test_item to be used in an array constructors.
 97  function tempfile_suite(name, tests) result(testitem)
 98    character(*), intent(in) :: name
 99    type(test_list), intent(in) :: tests
100    type(test_item) :: testitem
101
102    testitem = test_item(tempfile_test_suite(name=name, tests=tests))
103
104  end function tempfile_suite
105
106  
107  !> Initializes the test suite.
108  subroutine tempfile_test_suite_set_up(this)
109    class(tempfile_test_suite), intent(inout) :: this
110
111    allocate(this%env)
112    call init_test_env(this%env)
113    call check(this%env%unit /= -1, msg="Failed to open tempfile")
114    if (check_failed()) deallocate(this%env)
115
116  end subroutine tempfile_test_suite_set_up
117
118
119  !> Finalizes the test suite.
120  subroutine tempfile_test_suite_tear_down(this)
121    class(tempfile_test_suite), intent(inout) :: this
122
123    ! Explicit deallocation to trigger the finalizer of the test environment.
124    if (allocated(this%env)) deallocate(this%env)
125
126  end subroutine tempfile_test_suite_tear_down
127
128  
129  !> Wraps a tempfile_test_case instance as test_item to be used in an array constructors.
130  function tempfile_test(name, proc) result(testitem)
131    character(*), intent(in) :: name
132    procedure(test_tempfile_1) :: proc
133    type(test_item) :: testitem
134
135    testitem = test_item(tempfile_test_case(name=name, proc=proc))
136
137  end function tempfile_test
138
139
140  !> Runs test procedure with data from test suite.
141  subroutine tempfile_test_case_run(this)
142    class(tempfile_test_case), intent(in) :: this
143
144    type(test_ptr_item), allocatable :: scopeptrs(:)
145    type(test_env), pointer :: test_env_ptr
146
147    ! Get pointers to hosting scopes
148    ! scopeptrs(1): current scope - tempfile_test_case instance
149    ! scopeptrs(2): first enclosing scope - tempfile_test_suite instance
150    scopeptrs = scope_pointers()
151    call check(size(scopeptrs) >= 2)
152    if (check_failed()) return
153
154    ! Create pointer to data stored in the test suite.
155    test_env_ptr => null()
156    select type (suite => scopeptrs(2)%item)
157    type is (tempfile_test_suite)
158      test_env_ptr => suite%env
159    end select
160    call check(associated(test_env_ptr))
161    if (check_failed()) return
162
163    ! Call test routine with data from test suite.
164    call this%proc(test_env_ptr)
165
166  end subroutine tempfile_test_case_run
167
168end module test_mylib
169
170
171!> Test app driving Fortuno unit tests.
172program testapp
173  use test_mylib, only : tests
174  use fortuno_serial, only : execute_serial_cmd_app
175  implicit none
176
177  call execute_serial_cmd_app(tests())
178
179end program testapp