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_timefixes 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
outputslist 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¶
- Log files: the format the recorder writes and every format the replay reads.
- Run the simulator headless: recording, replay and fixtures from scripts.
- Recording and replaying a log: the same with the desktop application, including pause, step and seek.