Skip to content

Commit 5cf1ef1

Browse files
authored
Merge pull request #38 from UCL-ARC/add-pfunit-sytax-episode
2 parents f1918b7 + eb17ed0 commit 5cf1ef1

3 files changed

Lines changed: 288 additions & 127 deletions

File tree

config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ episodes:
6969
- 1-what-is-a-unit-tests.md
7070
- 2-refactor-fortran.md
7171
- 3-writing-your-first-unit-test.md
72-
- 4-fortran-unit-test-syntax.md
72+
- 4-pfunit-syntax.md
7373
- 5-debugging-a-broken-test.md
7474
- 6-testing-parallel-code.md
7575

episodes/4-pfunit-syntax.md

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
---
2+
title: "Fortran Unit Test Syntax"
3+
teaching:
4+
exercises:
5+
---
6+
7+
:::::::::::::::::::::::::::::::::::::: questions
8+
9+
- What is the syntax of writing a unit test in Fortran?
10+
- How do I build my tests with my existing build system?
11+
12+
::::::::::::::::::::::::::::::::::::::::::::::::
13+
14+
::::::::::::::::::::::::::::::::::::: objectives
15+
16+
- Able to write a unit test for a Fortran procedure with test-drive, veggies and/or pFUnit.
17+
- Understand the similarities between each framework and where they differ.
18+
19+
::::::::::::::::::::::::::::::::::::::::::::::::
20+
21+
22+
## What framework will we look at?
23+
24+
There are multiple frameworks available for writing unit tests in Fortran, as detailed on the
25+
[Fortran Lang website](https://fortran-lang.org/packages/programming/). However, we recommend
26+
the use of [pFUnit](https://github.com/Goddard-Fortran-Ecosystem/pFUnit) as it is...
27+
28+
- the most feature rich framework.
29+
- the most widely used framework.
30+
- being maintained.
31+
- able to integrate with CMake and make.
32+
33+
**Key features of pFUnit:**
34+
35+
- **Supports MPI**: Supports testing MPI parallelized code, including parametrizing tests by
36+
number of MPI ranks.
37+
- **Simple interface**: Tests are written in `.pf` format which is then pre-processed by a tool
38+
provided by pFUnit into `.f90` before compilation. This removes the need to write a lot of
39+
boilerplate code.
40+
41+
## The structure of a test module
42+
43+
All test modules share a basic structure...
44+
45+
```f90
46+
module test_something
47+
! use funit
48+
! use the src to be tested
49+
implicit none
50+
51+
! Derived types: Define types to act as test parameters and test cases.
52+
contains
53+
54+
! Test Suite: Define a test suite (collection of tests) to be returned from a procedure.
55+
56+
! Test Logic: Define the actual test execution code which will call the src and execute assertions.
57+
58+
! Type Constructors: Define constructors for your derived types (test parameters/cases).
59+
end module test_something
60+
```
61+
62+
## Let's dive into the syntax
63+
64+
We will continue to use the temperature conversion example from the previous episode to cover
65+
the syntax of pFUnit.
66+
67+
:::::::::::::::::::::::::::::::::::::::::::::::::::: spoiler
68+
69+
### Derived types:
70+
71+
This uses standard Fortran syntax to define some
72+
[derived types](https://fortran-lang.org/learn/quickstart/derived_types).
73+
74+
#### Test parameters
75+
76+
The test parameter type should contain the inputs and expected outputs of the code we are testing.
77+
78+
:::::::::::::::::::::::::::::: callout
79+
80+
#### Treat the src to be tested like a black box
81+
82+
When writing a unit test,
83+
84+
- The **inputs and outputs** are the important aspects to understand about our src code to be tested.
85+
- **The implementation should not influence how we write our test**. Not every test needs to be
86+
parametrized, but you will always need to consider the inputs and outputs of the src code you
87+
are testing.
88+
89+
::::::::::::::::::::::::::::::::::::::
90+
91+
Firstly, the test parameter derived-type is written as...
92+
93+
```F90
94+
@testParameter
95+
type, extends(AbstractTestParameter) :: my_test_params
96+
integer :: input, expected_output
97+
contains
98+
procedure :: toString => my_test_params_toString
99+
end type my_test_params
100+
```
101+
102+
**Key points:**
103+
104+
- Our parameter type must be decorated with **@testParameter** so that the pFUnit pre-processor
105+
understands that this derived type defines a test parameter.
106+
- We must extend one of the base types provided by pFUnit, in this case **AbstractTestParameter**
107+
which is the most generic.
108+
- We have declared a type-bound procedure **toString** which maps to the procedure
109+
**my_test_params_toString**. This allows pFUnit to log a helpful description of our parameter set
110+
which should be returned from **my_test_params_toString** (we'll see more on this later).
111+
112+
#### Test case
113+
114+
Then we can write our test case derived-type as...
115+
116+
```F90
117+
@TestCase(constructor=my_test_params_to_my_test_case, testParameters={my_test_suite()})
118+
type, extends(ParameterizedTestCase) :: my_test_case
119+
type(my_test_params) :: params
120+
end type my_test_case
121+
```
122+
123+
**Key points:**
124+
125+
- Our parameter type must be decorated with **@TestCase** so that the pFUnit pre-processor
126+
understands that this derived type defines a test case.
127+
- The **@TestCase** decorator includes some extra information to tell the pre-processor how
128+
the test case should be constructed. What we have defined is...
129+
- To convert from an instance of **my_test_params** to an instance of **my_test_case**, one
130+
must call **my_test_params_to_my_test_case**.
131+
- The list of parameter sets which define each individual parametrized test will be
132+
returned from the function **my_test_suite**
133+
- Just like with the test parameter type, we must extend one of the base types provided by
134+
pFUnit, in this case **ParameterizedTestCase** which indicates that this test should be
135+
parametrized.
136+
- We then define a single type-bound value which is of the test parameter type we have just
137+
defined.
138+
139+
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
140+
141+
:::::::::::::::::::::::::::::::::::::::::::::::::::: spoiler
142+
143+
### Test Suite:
144+
145+
In this section we define our parameter sets (or test suite). We define a function which
146+
returns our test parameters like so...
147+
148+
```F90
149+
function my_test_suite() result(params)
150+
type(my_test_params), allocatable :: params(:)
151+
152+
params = [ &
153+
my_test_params(1, 2), & ! Given input is 1, output is 2
154+
my_test_params(3, 4) & ! Given input is 3, output is 4
155+
]
156+
end function my_test_suite
157+
```
158+
159+
**Key points:**
160+
161+
- The function returns an array of **my_test_params**.
162+
- We are using a constructor function to define each parameter set which we do not need to
163+
define ourselves.
164+
165+
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
166+
167+
:::::::::::::::::::::::::::::::::::::::::::::::::::: spoiler
168+
169+
### Test Logic:
170+
171+
This is where we actually call our src procedure and carry out assertions...
172+
173+
```F90
174+
@Test
175+
subroutine TestMySrcProcedure(this)
176+
class (my_test_case), intent(inout) :: this
177+
178+
integer :: actual_output
179+
180+
call my_src_procedure(this%params%input, actual_output)
181+
182+
@assertEqual(this%params%input, actual_output, "Unexpected output from my_src_procedure")
183+
end subroutine TestMySrcProcedure
184+
```
185+
186+
**Key points:**
187+
188+
- We must decorate the test subroutine with the pFUnit annotation **@Test** so the pre-processor
189+
knows this is a test.
190+
- We are utilising a pre-processor directive provided by pFUnit **@assertEqual** which allows the
191+
exact comparison of two values (also works for comparing arrays). For a full list of the
192+
available assertion directives see
193+
[pFUnit documentation page for their preprocessor directives](https://pfunit.sourceforge.net/page_Assert.html)
194+
- As is done here, it is recommended to provide a helpful message in case of an assertion
195+
failing to help diagnose the issue.
196+
197+
::::::::::::::::::::::::::::::::::: callout
198+
199+
#### Parametrize on a test by test basis
200+
201+
It is also possible to parametrize a test at this point, instead of when defining the derived-types.
202+
This can be useful if you wish to reuse a test parameter type for multiple test cases...
203+
204+
```f90
205+
@Test(testParameters={my_test_suite()})
206+
subroutine TestMySrcProcedure(this)
207+
class (my_test_case), intent(inout) :: this
208+
...
209+
```
210+
211+
:::::::::::::::::::::::::::::::::::::::::::
212+
213+
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
214+
215+
:::::::::::::::::::::::::::::::::::::::::::::::::::: spoiler
216+
217+
### Type Constructors:
218+
219+
We are required to define two functions.
220+
221+
**A conversion from test parameters to a test case:**
222+
223+
```F90
224+
function my_test_params_to_my_test_case(testParameter) result(tst)
225+
type (my_test_case) :: tst
226+
type (my_test_params), intent(in) :: testParameter
227+
228+
tst%params = testParameter
229+
end function my_test_params_to_my_test_case
230+
```
231+
232+
It may be necessary to individually map each type-bound value within the
233+
**testParameter** to that in the **tst**, depending on their complexity.
234+
235+
236+
**A conversion from test parameters to a string:**
237+
238+
This function helps to provide a clearer description of each test case. The result
239+
of this function will be displayed alongside the name of the test for each parameter
240+
set.
241+
242+
```F90
243+
function my_test_params_toString(testParameter) result(string)
244+
class (my_test_params), intent(in) :: this
245+
character(:), allocatable :: string
246+
247+
character(len=80) :: buffer
248+
249+
write(buffer,'("Given ",i4," we expect to get ",i4)') this%input, this%expected_output
250+
string = trim(buffer)
251+
end function my_test_params_toString
252+
```
253+
254+
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
255+
256+
::::::::::::::::::::::::::::::::::::: challenge
257+
258+
## Challenge: Test temperature conversions using pFUnit
259+
260+
Continuing with part two of [3-writing-your-first-unit-test/challenge][exercises-challenge] from the
261+
exercises repo. Re-write your standard Fortran test using pFUnit.
262+
263+
:::::::::::::::::::::::::::::::: solution
264+
265+
A solution is provided in [3-writing-your-first-unit-test/solution](exercises-solution).
266+
267+
:::::::::::::::::::::::::::::::::::::::::
268+
:::::::::::::::::::::::::::::::::::::::::::::::
269+
270+
[pfunit-directives]: https://pfunit.sourceforge.net/page_Assert.html3-writing-your-first-unit-test/challenge/src/temp_conversions.f90
271+
[exercises-challenge]: https://github.com/UCL-ARC/fortran-unit-testing-exercises/tree/main/episodes/3-writing-your-first-unit-test/challenge
272+
[exercises-solution]: https://github.com/UCL-ARC/fortran-unit-testing-exercises/tree/main/episodes/3-writing-your-first-unit-test/solution

0 commit comments

Comments
 (0)