Unit testing in Fortran

FOSDEM 2026: Testing and Continuous Delivery devroom

Connor Aird

University College London

2026-01-31

Current state of Fortran

Some known Fortran projects

Repository Description Integration tests Unit tests
epochpic/epoch Popular particle-in-cell plasma code

ukaea/ReMKiT1D Framework for 1D multifluid and kinetic simulations of Tokamaks

geoschem/geos-chem Popular atmospheric chemistry code

MetOffice/ukca The UK’s main atmospheric chemistry code

MetOffice/lfric_core The Met Office’s next generation weather model

Why is testing Fortran hard

Note that these points are from my experience in academia.

  • Fortran is often legacy code.
    • Lots of global state.
    • Lack of units to test.
    • Locked into specific compilers or dependencies.
  • Many custom testing setups exist
    • Test frameworks do not benefit from open source contibutions.
  • Lack of know-how
    • Developers are often PhD students new to Fortran development.
  • Unclear which frameworks to choose.

Available frameworks

Fortran wiki

A list of known Fortran testing frameworks on the Fortran wiki

Fortran-lang

A list of Fortran libraries and tools on the fortran-lang website.

My chosen framework

The framework I tend to choose to work with is pFUnit, because…

  • Previous experience within our department.
  • Open source and still being developed.
  • Appears to be the most fully featured.
  • Most importantly, supports unit testing MPI parallelised code.

What is pFUnit

Overview of pFUnit

  • Created by developers from NASA.
  • Open source on GitHub and accepting contributions - Goddard-Fortran-Ecosystem/pFUnit
  • Availale under a NASA OPEN SOURCE AGREEMENT VERSION 1.3 licence
    • Permissive Licenses similar to MIT and BSD but there are differences.
  • Uses a preprocessor to convert .pf files to .f90.
  • Follows the style of JUnit. i.e. Directives (annotations in JUnit) label code sections for the preprocessor.
  • F2003 object oriented features and some F2008 features, thus requires more recent versions of compilers.

Syntax

.pf syntax is similar to .f90 with preprocessor directives, such as @Test and @assertEqual.

module test_something
    use maths_operations, only : double_int
    use funit
    implicit none

contains

    @Test
    subroutine test_double_int_1()
        integer :: input, actual_output

        input = 1

        call double_int(input, actual_output)

        @assertEqual(2, actual_output, "Unexpected output from double_int")
    end subroutine test_double_int_1
end module test_something

The preprocessor

  • Written in Python.
  • Converts .pf with directives into .f90.
  • Called automatically when building with Make or CMake.
  • Directives exist for the following tasks
    • Labeling custom types (test parameters and test cases).
    • Labeling test subroutines.
    • Specifying assertions.

Assert preprocessor directives

Integrating with build systems

pFUnit can be integrated into both Make and CMake build systems using functions provided by pFUnit

Integrating with Make

# Include PFUNIT.mk from a locally installed version of pFUnit
PFUNIT_INCLUDE_DIR ?= $(ROOT_DIR)/../pfunit/build/installed/PFUNIT-4.12/include
include $(PFUNIT_INCLUDE_DIR)/PFUNIT.mk
TEST_FLAGS = $(PFUNIT_EXTRA_FFLAGS) -I$(BUILD_DIR) $(FC_FLAGS) $(LIBS)

# Define variables to be picked up by make_pfunit_test
tests_TESTS = \
  test_something.pf \
  test_something_else.pf
tests_OTHER_SOURCES = $(filter-out $(BUILD_DIR)/main.o, $(SRC_OBJS))
tests_OTHER_LIBRARIES = $(TEST_FLAGS)

# Triggers pre-processing and defines rule for building test executable
$(eval $(call make_pfunit_test,tests))

# Converts pre-processed test files into objects ready for building of the executable
%.o: %.F90
    $(FC) -c $(TEST_FLAGS) $<

Integrating with CMake

find_package(PFUNIT REQUIRED)

# Filter out the main.f90 files. We can only have one main() function in our tests
set(PROJ_SRC_FILES_EXEC_MAIN ${PROJ_SRC_FILES})
list(FILTER PROJ_SRC_FILES_EXEC_MAIN EXCLUDE REGEX ".*main.f90")

# Create library for src code
add_library (sut STATIC ${PROJ_SRC_FILES_EXEC_MAIN})

# List all test files
set(test_srcs
  "${PROJECT_SOURCE_DIR}/tests/test_something.pf"
  "${PROJECT_SOURCE_DIR}/tests/test_something_else.pf"
)

# Triggers pre-processing and defines rule for building test executable
add_pfunit_ctest (test_something_interesting
  TEST_SOURCES ${test_srcs}
  LINK_LIBRARIES sut # your application library
  )

Automated testing with pFUnit in CI (GitHub Actions)

Automating with Docker

To ensure a consitent environment when running pFUnit tests through GitHub actions, one can make use of containerisation (Docker, in my case)…

  • Within a Dockerfile…
    • Install and build depenencies.
    • Build src and test objects and executables.
    • Run all tests.
  • Wrap slow changing dependencies, such as pFUnit, into a parent Dockerfile to be built less frequently.
  • Proprietary software can be difficult to build within Dockerfiles.

See UCL-ARC/fortran-unit-testing-exercises for an example of this being used in practice

Example Dockerfile

FROM parent-image:main

# Copy src and test code into the container
COPY my-project /my-project
WORKDIR /my-project

# Build tests with cmake
RUN cmake -B build -DCMAKE_PREFIX_PATH=/path/to/pfunit/build/installed && \
    cmake --build build

# Run pfunit tests with ctest
RUN ctest --test-dir build --output-on-failure

Example workflow

---
name: Build and test with pFUnit

on:
  push:
    branches:
      - main

jobs:
  testing:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: read
      attestations: read
      id-token: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Build dockerfile
        run: docker build .

Future Work

Fortran testing workshop (UCL)

I have been developing a software carpentry style lesson for unit testing in Fortran.

Conclusions

  • Fortran is still relevant.
  • Popular Fortran projects are often lacking unit tests.
  • It is recommended to use pFUnit for Fortran unit tests.
  • pFUnit…
    • uses a preprocessor to convert .pf files to .f90.
    • assertions are provided as preprocessor directives.
    • integrates with Make and CMake.
  • Automatic pFUnit testing can be done within CI pipelines using containerisation.
    • Can be difficult if relying on Legacy or proprietary dependencies.

Acknowledgements

Scan to view the slides