Writing Tests That Pass on Windows¶
Most Windows-only test failures in Go projects come from a handful of platform differences. They are cheap to avoid when writing the code and tedious to diagnose from a CI log afterward. This guide lists them with the portable alternative for each.
Wall-clock ties¶
The difference: time.Now() on Linux and macOS has nanosecond
resolution. On Windows the clock advances in steps of roughly 0.5 to 15.6 ms,
so values taken in quick succession are often identical.
What breaks:
- Ordering rows by
created_at(or anytime.Now()column), then asserting the exact order. Equal timestamps fall through to the next sort key. if after.After(before)between two operations that run back to back.if elapsed > 0for a measured duration.
The pattern that bites hardest: ORDER BY created_at, id where id is a
random UUID. On Unix the timestamps differ and the test always passes. On
Windows they tie, id decides, and the test fails about half the time for two
rows.
What to do:
- Give ordered records a tie-breaker that follows insertion order, not a
random one: a UUIDv7 (
uuid.NewV7()fromgithub.com/google/uuid), a database sequence, or an explicit sequence column. UUIDv7 values created by one process keep increasing even within a millisecond, and they sort correctly asuuidin PostgreSQL and as text in SQLite. - If the order within a tie truly does not matter to callers, say so in the
doc comment and make the test compare as a set (
slices.Sortboth sides), so the test checks what the code promises. - Assert
d >= 0, notd > 0, for durations.testutil.AssertDurationNonNegativedoes this. - Never
time.Sleepfor a few milliseconds to force a different timestamp. It lowers the odds without removing the failure.
Path separators and permissions¶
- Build paths with
filepath.Join, and compare withfilepath.ToSlashwhen a test expects a fixed string.fs.NormalizePathandfs.OSPathconvert. - Do not check a path that came from a document or an archive with
filepath.IsAbs: it gives a different answer on each platform. Usefs.ValidatePath,fs.ValidateSlashPathorfs.IsAbsAny, which give the same answer everywhere. - Keep path test cases out of
runtime.GOOSbranches. A case that runs only where it passes cannot show that another platform disagrees; run every case everywhere and give each the answer it should have on all of them. - File mode bits other than read-only are not enforced on Windows. Do not
assert
0600after a write, and skip permission-denied tests withfs.PermissionsEnforced()instead of aruntime.GOOScheck, so the condition is named once.fs.CheckPrivateis the owner-only assertion that is correct on both.fs.IsExecutablereplaces mode checks. - Home and config directories use
USERPROFILEandAPPDATA, notHOME.testutilandpathsset and resolve both.
Files that are still open¶
Windows refuses to delete or rename an open file. Close files and databases
before t.TempDir cleanup runs, using t.Cleanup in reverse order of
opening, and check the Close error.
This applies to a test that simulates a crash by returning early, for example from a hook between "temporary file created" and "renamed". A real crash closes the process's handles; the simulation must close the handle too, and leave the file, or Windows cannot remove it and the next step and the temporary directory cleanup both fail.
File locks and directory flushes¶
Use fs.Lock rather than syscall.Flock, which does not exist on Windows, and
fs.SyncDir rather than opening a directory to Sync it, which fails there.
Both are tested against a second process in this module.
Process and signals¶
There is no SIGTERM, and process groups work differently. Use
process.Signal and process.SetDetached rather than syscall directly.
Line endings¶
Git may check out text files with CRLF. Read fixtures with
testutil.ReadFileLF and compare golden files with testutil.AssertGolden,
which ignore CRLF versus LF. In a repository that compares files byte for
byte, also set * text=auto eol=lf in .gitattributes.
Catching these before CI¶
- Run the test suite with
-count=20for any test that orders rows or compares times. Ties show up on Unix too when the operations are fast enough, and repetition exposes order assumptions that happen to pass. - Keep a Windows job in CI from the first commit rather than adding it when the project is mature.
- Review any
ORDER BYwith a non-deterministic final key.