Skip to content

Commit ddeff76

Browse files
committed
Add solution for temp conversions exercise
1 parent 6f65c17 commit ddeff76

File tree

4 files changed

+122
-2
lines changed

4 files changed

+122
-2
lines changed

episodes/6-writing-your-first-unit-test/standard-fortran/challenge/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Fortran without the use of a testing framework, in order to teach the principles
66

77
## The src code
88

9-
in [src](./src) you will find a library [temp_conversions.f90](./src/temp_conversions.f90)
9+
In [src](./src) you will find a library [temp_conversions.f90](./src/temp_conversions.f90)
1010
which provides functions for converting between various units of temperature. The functions
1111
provided are...
1212

episodes/6-writing-your-first-unit-test/standard-fortran/challenge/test/test_temp_conversions.f90

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,6 @@ subroutine test(passed, failure_message)
3535

3636
! Populate the failure message
3737
write(failure_message, '(A,A)') "It is useful to include input, expected output and actual output values here. To do ", &
38-
"that, replace (A,A) with the correct format for your values, for example (A,I3,A,I3,A,I3)."
38+
"that, replace (A,A) with the correct format for your values, for example (A,F7.2,A,F7.2,A,F7.2)."
3939
end subroutine test
4040
end program test_temp_conversions
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Writing your first unit test - Standard Fortran - Solution
2+
3+
The solution is provided in the form of a single test file [test_temp_conversions.f90](./test_temp_conversions.f90)
4+
which replaces the file of the same name provided in the [challenge/test](../challenge/test/) directory.
5+
6+
## Key points
7+
8+
There are several key aspects within the solution that are important to implement in any test.
9+
10+
### Isolated test subroutine
11+
12+
Each test subroutine in [test_temp_conversions.f90](./test_temp_conversions.f90) calls and tests only one src
13+
function. For example, `test_fahrenheit_to_celsius` calls and tests `fahrenheit_to_celsius` and
14+
`test_celsius_to_kelvin` calls and tests `celsius_to_kelvin`.
15+
16+
This is important as, in the event of a test failing it will be clear which src function is the cause of the failure.
17+
If instead we had implemented a single test subroutine which calls both `test_fahrenheit_to_celsius` and
18+
`fahrenheit_to_celsius`, then a failure in such a test could have been caused by either of those src functions and further
19+
investigation would be required.
20+
21+
### Parameterised tests
22+
23+
Each test subroutine in [test_temp_conversions.f90](./test_temp_conversions.f90) take in an `input` and an `expected_output`.
24+
This allows the same test subroutine to be called with multiple different inputs to test several scenarios with the same test
25+
code. Therefore, we are able to test edge cases and other key scenarios more easily.
26+
27+
### Clear failure message
28+
29+
Each test subroutine in [test_temp_conversions.f90](./test_temp_conversions.f90) populates `failure_message` with a clear
30+
message that aims to make it as easy as possible to diagnose a failing test. Importantly, the `failure_message` includes the
31+
`input`, the `expected_output` and the actual value which we have compared to the `expected_output`.
32+
33+
### Comparing floats within an appropriate tolerance
34+
35+
We cannot compare two floats directly as due to rounding errors they will almost always
36+
not be exactly the same. Therefore, we must check the difference between two floats that
37+
we expect to be equal and ensure it is less than some appropriate tolerance. This tolerance
38+
should be as small as possible whilst still making sense with the code we are testing.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
program test_temp_conversions
2+
use temp_conversions, only : fahrenheit_to_celsius, celsius_to_kelvin
3+
implicit none
4+
5+
! The tolerance to use when comparing floats
6+
real, parameter :: tolerance = 1e-6
7+
8+
integer :: i
9+
10+
! Declare passed and failure message arrays to be set by a test subroutine(s)
11+
logical :: passed(7)
12+
character(len=200) :: failure_message(7)
13+
14+
call test_fahrenheit_to_celsius(0.0, -17.77778, passed(1), failure_message(1))
15+
call test_fahrenheit_to_celsius(32.0, 0.0, passed(2), failure_message(2))
16+
call test_fahrenheit_to_celsius(-100.0, -73.33, passed(3), failure_message(3))
17+
call test_fahrenheit_to_celsius(1.23,-17.09, passed(4), failure_message(4))
18+
19+
call test_celsius_to_kelvin(0.0, 273.15, passed(5), failure_message(5))
20+
call test_celsius_to_kelvin(-273.15, 0.0, passed(6), failure_message(6))
21+
call test_celsius_to_kelvin(-173.15, 100.0, passed(7), failure_message(7))
22+
23+
if (all(passed)) then
24+
write(*,*) "All tests passed!"
25+
else
26+
do i = 1, size(passed)
27+
if (.not. passed(i)) then
28+
write(*,*) "FAIL: ", trim(failure_message(i))
29+
end if
30+
end do
31+
stop 1
32+
end if
33+
34+
contains
35+
!> Unit test subroutine for celsius_to_kelvin
36+
subroutine test_fahrenheit_to_celsius(input, expected_output, passed, failure_message)
37+
!> The input fahrenheit value to pass to fahrenheit_to_celsius
38+
real, intent(in) :: input
39+
!> The celsius value we expect to be returner from fahrenheit_to_celsius
40+
real, intent(in) :: expected_output
41+
!> A logical to track whether the test passed or not
42+
logical, intent(out) :: passed
43+
!> A failure message to be displayed if passed is false
44+
character(len=200), intent(out) :: failure_message
45+
46+
real :: actual_output
47+
48+
! Get the actual celsius value returned from fahrenheit_to_celsius
49+
actual_output = fahrenheit_to_celsius(input)
50+
51+
! Check that the actual value is within some tolerance of the expected value
52+
passed = abs(actual_output - expected_output) < tolerance
53+
54+
! Populate the failure message
55+
write(failure_message, '(A,F7.2,A,F7.2,A,F7.2)') "Failed With ", input, ": Expected ", expected_output, " but got ", &
56+
actual_output
57+
end subroutine test_fahrenheit_to_celsius
58+
59+
!> Unit test subroutine for celsius_to_kelvin
60+
subroutine test_celsius_to_kelvin(input, expected_output, passed, failure_message)
61+
!> The input celsius value to pass to celsius_to_kelvin
62+
real, intent(in) :: input
63+
!> The kelvin value we expect to be returner from celsius_to_kelvin
64+
real, intent(in) :: expected_output
65+
!> A logical to track whether the test passed or not
66+
logical, intent(out) :: passed
67+
!> A failure message to be displayed if passed is false
68+
character(len=200), intent(out) :: failure_message
69+
70+
real :: actual_output
71+
72+
! Get the actual celsius value returned from celsius_to_kelvin
73+
actual_output = celsius_to_kelvin(input)
74+
75+
! Check that the actual value is within some tolerance of the expected value
76+
passed = abs(actual_output - expected_output) < tolerance
77+
78+
! Populate the failure message
79+
write(failure_message, '(A,F7.2,A,F7.2,A,F7.2)') "Failed With ", input, ": Expected ", expected_output, " but got ", &
80+
actual_output
81+
end subroutine test_celsius_to_kelvin
82+
end program test_temp_conversions

0 commit comments

Comments
 (0)