Skip to content

Building a test fixture from a recorded log

In this tutorial you turn a simulated session into a test fixture: a small log file, committed next to your tests, that replays the same sentences every time and drives an automated test of the software that consumes them. It takes about fifteen minutes and uses only the command-line tool, nmeasim, and Python for the test.

You need nmeasim in your PATH (install it; the AppImage runs it as NMEASimulatorX-<version>-<arch>.AppImage nmeasim), Python 3.10 or later and pip install pynmea2 pytest.

1. Make a profile for the scenario

A fixture should describe one situation, not whatever the simulator happened to do. Start from the default profile:

nmeasim profile init harbour.json

Open harbour.json and change three things in it:

"name": "Harbour approach fixture",
"outputs": [],
"simulation": {
  "start_time": "2026-09-23T10:00:00.000Z",
  ...
}
  • start_time fixes the simulated clock, so the time and date fields of every sentence start at 10:00:00 UTC instead of now.
  • random_seed (2026 by default) already makes the drift of heading, speed, depth and wind the same in every run.
  • The empty outputs list keeps the recording from opening a TCP server.

Anything else about the scenario goes into the same file: a shallower simulation.seed.depth_m, a destination for APB and RMB, engines, or sentences switched off. The profile reference lists every key.

2. Record a session

nmeasim run --profile harbour.json --record session.log --duration 20 --quiet

The run takes twenty seconds and writes every sentence to session.log:

# NMEA Simulator X log 1
# recorded: 2026-09-23T18:10:51.640Z
# profile: Harbour approach fixture
2026-09-23T18:10:51.742Z $GPRMC,100000.10,A,3759.0281,N,02343.6502,E,6.5,45.0,230926,4.6,E,A*0D
2026-09-23T18:10:51.742Z $GPGGA,100000.10,3759.0281,N,02343.6502,E,1,08,0.9,0.0,M,0.0,M,,*59

The prefix of each line is the moment the sentence was sent; the replay uses it for timing. The time fields inside the sentences come from the simulated clock you fixed, so the first fix is at 10:00:00.10 and the next ones on every whole second. The simulation steps are measured with the computer's clock, though, and the drifting values integrate those steps: a second recording can differ in a last digit, for example an apparent wind angle of 255.9 one time and 256.0 the next. That is why a fixture is recorded once and committed, not regenerated by every test run.

3. Cut it to size

Keep only what the test needs. Replaying the session for a limited time while recording again produces the first ten seconds as a new log:

nmeasim run --replay session.log --duration 10 --record harbour-approach.log --quiet

The file is plain text, so you can also delete lines in an editor: keep the three header lines and any contiguous run of sentences. The same works for a log captured from a real instrument, for example with nc plotter.local 10110 > capture.nmea; the log file reference lists every line shape the replay reads.

4. Check it with an independent parser

Before a fixture becomes the reference for other tests, make sure every sentence in it is valid. The repository's check script parses each one with pynmea2, which is developed independently of this project, and checks checksums and the 82-character limit:

nmeasim run --replay harbour-approach.log --stdout --quiet | python3 tools/check_nmea_stream.py
checked 283 sentences, 0 failures, 22 formatters: DBT, DPT, GGA, GLL, GSA, GSV, HDG, HDM, ...

5. See that the replay is repeatable

nmeasim run --replay harbour-approach.log --stdout --quiet > first.nmea
nmeasim run --replay harbour-approach.log --stdout --quiet > second.nmea
cmp first.nmea second.nmea && echo identical

The replay sends the recorded sentences unchanged, on the recorded cadence, and ends by itself when the log does, with exit status 0. The standard output frames each sentence with CR LF, as NMEA 0183 requires.

6. Use it in a test

Put the log under your test fixtures, for example tests/fixtures/harbour-approach.log, and write a test that replays it into the code under test. Here pynmea2 stands in for your own parser:

import pathlib
import subprocess

import pynmea2

FIXTURE = pathlib.Path(__file__).parent / "fixtures" / "harbour-approach.log"


def replay(path):
    """Plays the log on its recorded cadence and returns the sentences."""
    result = subprocess.run(["nmeasim", "run", "--replay", str(path), "--stdout", "--quiet"],
                            check=True, capture_output=True, text=True)
    return result.stdout.splitlines()


def test_the_vessel_heads_north_east():
    fixes = [pynmea2.parse(line) for line in replay(FIXTURE) if line.startswith("$GPRMC")]
    assert len(fixes) == 11  # 10:00:00.10, then every whole second up to 10:00:10
    assert fixes[-1].latitude > fixes[0].latitude
    assert fixes[-1].longitude > fixes[0].longitude
python3 -m pytest -q
.                                                                        [100%]
1 passed in 9.97s

The test takes as long as the recording because the replay keeps its timing. When timing does not matter, read the sentences straight from the file instead; this takes milliseconds:

grep -v '^#' harbour-approach.log | cut -d' ' -f2- > harbour-approach.nmea

When the code under test reads from the network, replay to it instead of to standard output, for example --tcp-server 10110 or a udp output; the replay starts sending at once, so start the consumer first or give it a moment to connect.

7. Commit the fixture

Commit harbour-approach.log and the profile that produced it, so that the scenario can be recorded again deliberately when it has to change. In CI, install nmeasim from the release packages (the AppImage needs no installation) and run the tests as above.

Where next