12 KiB
Testing Patterns
Analysis Date: 2026-02-26
Test Framework
Runner:
- Go standard
testingpackage - No external test frameworks (no testify assertions — uses raw
t.Errorf/t.Fatalf) - golangci-lint
testifylintis enabled but unused (no testify dependency)
Assertion Library:
- Standard library only —
t.Errorf,t.Fatalf,t.Fatal,t.Logf - Custom equality helpers in test files (e.g.,
slicesEqual)
Run Commands:
make test # All tests (preferred)
go test -tags webkit2_41 -race -count=1 -timeout 120s ./... # All tests manually
go test -tags webkit2_41 ./backend/player/ # Single package
go test -tags webkit2_41 -run TestFunctionName ./backend/player/ # Single test
go test -tags webkit2_41 -v -run TestFunctionName ./backend/... # Verbose single test
Build Tags Requirement
Critical: All go test invocations require -tags webkit2_41. The Makefile handles this automatically. Without this tag, compilation fails because the Wails v2 framework depends on WebKit bindings.
# Correct:
go test -tags webkit2_41 ./...
# Wrong — will fail to compile:
go test ./...
The Makefile test target includes all recommended flags:
test:
go test -tags webkit2_41 -race -count=1 -timeout 120s ./...
-race— Race detector enabled-count=1— Disable test caching (always run)-timeout 120s— 2-minute timeout
Test File Organization
Location: Colocated with source as *_test.go in the same package:
backend/player/player.go
backend/player/player_test.go
backend/metadata/genre.go
backend/metadata/genre_test.go
backend/metadata/mp3duration.go
backend/metadata/mp3duration_test.go
backend/metadata/flacduration.go
backend/metadata/flacduration_test.go
backend/coverart/coverart.go
backend/coverart/coverart_test.go
backend/playlist/m3u.go
backend/playlist/m3u_test.go
backend/playlist/match.go
backend/playlist/match_test.go
Exception: backend/coverart/coverart_test.go uses package coverart_test (external test package) to test only the exported API.
All other test files use the same package as the source (internal tests), allowing access to unexported functions:
package metadata // internal test — can call unexported getMP3Duration()
package playlist // internal test — can call unexported sanitizeFilename()
Test Fixtures
Location: test_data/ at the project root.
Contents: Real audio files (MP3, FLAC) used by metadata and player tests.
Access pattern: Tests use relative paths from the package directory:
// From backend/player/player_test.go
var testQueue = []string{
"../../test_data/music_library_test/other_music/03 PONPONPON.mp3",
"../../test_data/music_library_test/01 Some Chords.mp3",
"../../test_data/music_library_test/03 anything.mp3",
}
// From backend/metadata/mp3duration_test.go
root := filepath.Join("..", "..", "test_data")
Test helper functions scan the fixture directory for files of the right type:
// backend/metadata/mp3duration_test.go
func testMP3Files(t *testing.T) []string {
t.Helper()
root := filepath.Join("..", "..", "test_data")
var files []string
err := filepath.Walk(root, func(
path string, info os.FileInfo, err error,
) error {
if !info.IsDir() && filepath.Ext(path) == ".mp3" {
files = append(files, path)
}
return nil
})
if len(files) == 0 {
t.Skip("no .mp3 test fixtures found in test_data/")
}
return files
}
// backend/metadata/flacduration_test.go
func testFlacFiles(t *testing.T) []string {
t.Helper()
root := filepath.Join("..", "..", "test_data")
// same pattern for .flac files
}
t.TempDir() is used for tests that write files:
dir := t.TempDir()
tmpPath := filepath.Join(dir, "multi_id3v2.mp3")
os.WriteFile(tmpPath, out, 0o644)
Hardware-Dependent Test Skipping
Integration Tests (Audio Device + Wails Runtime)
The player test requires both a Wails runtime context and an audio output device. It skips unless explicitly opted in:
// backend/player/player_test.go
func TestPlayer(t *testing.T) {
if os.Getenv("YELLOWJACKET_INTEGRATION") == "" {
t.Skip(
"skipping: integration test requires Wails runtime and audio device " +
"(set YELLOWJACKET_INTEGRATION=1 to run)",
)
}
// ...
}
To run integration tests:
YELLOWJACKET_INTEGRATION=1 go test -tags webkit2_41 -v ./backend/player/
Fixture-Dependent Tests
Tests that need audio fixtures skip gracefully when none are found:
if len(files) == 0 {
t.Skip("no .mp3 test fixtures found in test_data/")
}
Test Structure Patterns
Table-Driven Tests
The predominant pattern across the codebase. Use a slice of anonymous structs with t.Run subtests:
// backend/metadata/genre_test.go
func TestParseGenres(t *testing.T) {
t.Parallel()
tests := []struct {
name string
raw string
want []string
}{
{
name: "single genre",
raw: "Rock",
want: []string{"Rock"},
},
{
name: "semicolon separated",
raw: "Rock; Electronic",
want: []string{"Rock", "Electronic"},
},
// ...
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := ParseGenres(tt.raw)
if !slicesEqual(got, tt.want) {
t.Errorf(
"ParseGenres(%q) = %v, want %v",
tt.raw, got, tt.want,
)
}
})
}
}
Parallel Tests
Use t.Parallel() at both the suite and subtest level. All unit tests use parallel execution:
func TestSanitizeFilename(t *testing.T) {
t.Parallel() // top-level parallel
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // subtest parallel
// ...
})
}
}
File-Iteration Tests
For tests that iterate over real fixture files, use t.Run with the filename:
// backend/metadata/mp3duration_test.go
func TestGetMP3Duration_MatchesBeepDecode(t *testing.T) {
for _, path := range testMP3Files(t) {
t.Run(filepath.Base(path), func(t *testing.T) {
// compare fast parser vs full decode
refMS, err := GetTrackLengthMillis(path)
// ...
if diffMS > toleranceMS {
t.Errorf(
"duration mismatch: beep=%dms fast=%dms "+
"(diff %dms exceeds %dms tolerance)",
refMS, fastMS, diffMS, toleranceMS,
)
}
})
}
}
Integration Test Pattern
The player integration test creates a real player instance and exercises it:
// backend/player/player_test.go
func TestPlayer(t *testing.T) {
if os.Getenv("YELLOWJACKET_INTEGRATION") == "" {
t.Skip("skipping: integration test requires ...")
}
p := NewPlayer(slog.Default(), nil)
if err := p.InitSpeaker(); err != nil {
t.Fatalf("could not initialize speaker: %s", err.Error())
}
p.SetContext(t.Context())
for _, track := range testQueue {
if err := p.LoadFile(track); err != nil {
t.Fatalf("could not load file %s: %s", track, err.Error())
}
if err := p.Play(); err != nil {
t.Fatalf("could not play file %s: %s", track, err.Error())
}
}
}
Mocking
No mocking framework is used. The codebase relies on:
- Interfaces for injection: The
TrackLoaderinterface inbackend/queue/queue.goallows the queue to work with any player implementation:
type TrackLoader interface {
LoadFile(filePath string) error
Play() error
IsPlaying() bool
CurrentPositionSeconds() (int, error)
UnloadTrack()
}
nildependencies: Tests passnilfor dependencies not needed:
p := NewPlayer(slog.Default(), nil) // nil database
-
Real implementations: Most tests exercise real code against test fixtures rather than mocks.
-
Callback injection: Cross-cutting behavior uses function callbacks rather than interface mocks:
// Injected callback avoids queue→player circular dependency:
p.SetPlaybackFinishedHandler(handler func())
// Hook-based coordination:
l.SetRescanHooks(library.RescanHooks{
PreClear: yj.queue.Clear,
PostScan: yj.playlist.RestoreAllPlaylists,
})
Test Helpers
Custom Equality Functions
Since no assertion library is used, test files include local equality helpers:
// backend/metadata/genre_test.go
func slicesEqual(a, b []string) bool {
if len(a) == 0 && len(b) == 0 {
return true
}
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
// backend/playlist/match_test.go
func stringSliceEqual(a, b []string) bool {
// identical implementation
}
Test File Builders
The buildID3v2Header helper in backend/metadata/flacduration_test.go creates synthetic audio file structures for testing:
func buildID3v2Header(payloadSize int) []byte {
header := []byte{
'I', 'D', '3', // signature
3, 0, // version 2.3.0
0, // flags
0, 0, 0, 0, // size (syncsafe, filled below)
}
header[6] = byte((payloadSize >> 21) & 0x7F)
header[7] = byte((payloadSize >> 14) & 0x7F)
header[8] = byte((payloadSize >> 7) & 0x7F)
header[9] = byte(payloadSize & 0x7F)
return header
}
t.Helper() Usage
Test helper functions call t.Helper() so failure line numbers point to the caller:
func testMP3Files(t *testing.T) []string {
t.Helper()
// ...
}
t.Context() Usage
Integration tests use t.Context() for the test context (enforced by usetesting linter):
p.SetContext(t.Context())
//nolint Annotations
Tests use //nolint:mnd for magic numbers in test data construction:
//nolint:mnd // synthetic tag construction.
tag1Size := 1024
tag2Size := 2048
//nolint:mnd // expected offset after first tag.
expectedFirst := int64(10 + 100)
//nolint:mnd // byte values from manual FLAC spec packing.
var si [streamInfoLength]byte
si[10] = 0x0A
Error Assertion Patterns
Fatal vs Error
t.Fatalffor setup failures that prevent the test from continuingt.Errorffor check failures that should be reported but allow remaining checks to run
// Setup failure — stop immediately:
f, err := os.Open(path)
if err != nil {
t.Fatalf("open: %v", err)
}
// Assertion failure — continue checking other fields:
if got != tt.want {
t.Errorf(
"SizedFilename(%q, %q) = %q, want %q",
tt.filename, tt.suffix, got, tt.want,
)
}
Error Expectation
Tests that expect errors check for nil/non-nil:
func TestWriteM3U8EmptyDir(t *testing.T) {
t.Parallel()
err := writeM3U8("", 1, "test", nil)
if err == nil {
t.Fatal("expected error for empty dir path")
}
}
Frontend Type Checking
No frontend test framework is configured. TypeScript correctness is verified via type checking:
cd frontend && pnpm exec tsc --noEmit
This validates all TypeScript files against the strict tsconfig.json settings without producing output files.
Test Coverage
Requirements: No enforced coverage target.
Coverage command:
go test -tags webkit2_41 -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
Test Types Summary
Unit Tests:
- All tests in
backend/metadata/,backend/coverart/,backend/playlist/ - Test pure functions with table-driven patterns
- Use
t.Parallel()for concurrent execution - No external dependencies (except test fixtures)
Integration Tests:
backend/player/player_test.go- Requires audio hardware and Wails runtime
- Gated behind
YELLOWJACKET_INTEGRATION=1env var - Not run in CI
E2E Tests:
- Not implemented
Frontend Tests:
- Not implemented (type checking only via
tsc --noEmit)
Testing analysis: 2026-02-26