Fyne — Testing#
Test metrics#
- Test files: 279
- Source files (non-test): 586 (865 total .go files minus 279 test files)
- Ratio (test / source): ~1:2.1 (one test file per two source files)
- Test frameworks: stdlib
testing+github.com/stretchr/testify/assert+github.com/stretchr/testify/require; no gomock, ginkgo, gocheck, or goconvey - Coverage floor: 62% enforced in CI (
platform_tests.ymlfails the build if coverage drops below this threshold)
Test organization#
- Placement: Both same-package and external
_testpackage. Inwidget/alone: 45 internal (package widget) vs 20 external (package widget_test) test files. Internal tests access unexported fields directly; external tests treat the package as a black box. - Helper packages:
fyne.io/fyne/v2/test(public): A full fake driver/app/canvas stack. ProvidesNewApp()(initializes a headlessfyne.App),NewCanvas(),NewWindow(), interaction simulators (Tap,TapAt,TapCanvas,TapSecondary,DoubleTap,Type,TypeOnCanvas,Drag,Scroll,MoveMouse,FocusNext), golden-file assertions (AssertRendersToMarkup,AssertRendersToImage,AssertObjectRendersToMarkup,AssertObjectRendersToImage), and theme utilities (ApplyTheme,WithTestTheme). This package is intended for use by both the framework and application developers.fyne.io/fyne/v2/internal/test(private): Lower-level pixel utilities (AssertImageMatches,pixCloseEnough,NewCheckedImage). Called by the publictestpackage;pixCloseEnoughimplements a 4-delta per-pixel + 1% total-pixel tolerance to handle platform rendering variation (notably Darwin/arm64).
- Fixtures: 18
testdata/directories spread across all major packages (widget/,canvas/,container/,dialog/,theme/,app/,internal/driver/glfw/,internal/painter/,internal/svg/, etc.). Each testdata directory holds both.pnggolden images and.xmlmarkup snapshots.
Test patterns#
Table-driven tests#
- Prevalence: Moderate. 328 occurrences of table-driving constructs (
t.Run,tests := []struct,tc.name). Many widget tests are written as one function per scenario (TestButton_Tapped,TestButton_SetText,TestButton_MinSize_Icon) rather than consolidated into table form. Table-driven style is used heavily inwidget/richtext_test.goandwidget/radio_group_internal_test.gofor exhaustive input/output cases. - Style: Anonymous struct with a
namefield and typedfields/args/wantdecomposition:tests := []struct { name string fields fields args args want string }{ ... } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ... }) } - Example:
widget/richtext_test.go:230— tests for buffer insert/delete operations at various positions.
Golden file testing (dominant pattern)#
- Prevalence: 647 calls to
AssertRendersToMarkup/AssertRendersToImage/AssertObjectRendersToMarkup/AssertObjectRendersToImageacross the test suite. This is the most heavily used assertion strategy. - How it works: The
test.AssertRendersToMarkupfunction renders the canvas using the headless software painter, serializes the widget tree to XML (snapshot(c)viamarkupRenderer), then byte-compares against a stored.xmlfile intestdata/. On first run (no master), the generated file is written totestdata/failed/for human review. Adiff-failed.shscript exists inwidget/testdata/to help reviewers compare expected vs actual. - XML format example (
widget/testdata/button/layout_text_only_leading_leading.xml):<canvas padded size="150x200"> <content> <widget pos="4,4" size="142x192" type="*widget.Button"> <rectangle fillColor="button" radius="4" size="142x192"/> <widget pos="8,86" size="28x19" type="*widget.RichText"> <text alignment="center" bold size="28x19">Test</text> </widget> </widget> </content> </canvas> - PNG golden files: Used for pixel-level rendering tests (e.g.,
button/initial.png,button/hovered.png,button/disabled.png). ThepixCloseEnoughfunction ininternal/testallows a 4-value delta per channel and up to 1% total pixel mismatches, preventing flaky CI failures from platform-specific anti-aliasing. - Assessment: The XML markup format is more maintainable than PNG-only testing: diffs are readable, merge conflicts are resolvable, and the format captures widget tree structure (types, positions, sizes) rather than just pixels. PNG golden files are reserved for painter-level tests where pixel accuracy matters.
Mocking approach#
- Strategy: No mock code generator (no gomock, mockery, or interface{}). The entire
test/package is the mocking layer — it provides full fake implementations offyne.App,fyne.Driver,fyne.Canvas,fyne.Window,fyne.Clipboard,fyne.Preferences,fyne.Storage, andfyne.CloudProvideras concrete structs. - Example:
test/app.godefinestype app struct { driver *driver; settings *testSettings; ... }which satisfiesfyne.App.test/driver.godefinestype driver struct { painter SoftwarePainter; windows []fyne.Window; ... }which satisfiesfyne.Driver. The fake driver’sDoFromGoroutineexecutes functions inline (no main-thread marshaling), making tests single-threaded and deterministic. - Design quality: The fake implementations are in a public, versioned package (
fyne.io/fyne/v2/test). This means third-party widget authors and application developers get the same testing infrastructure as the framework itself — a strong ecosystem commitment.
Integration tests#
- Present: Yes, at the GLFW driver level.
internal/driver/glfw/tests use the real GLFW windowing system withxvfb-run(virtual framebuffer) on Linux CI. - How:
TestMainininternal/driver/glfw/window_test.gostarts the GLFW event loop on the main OS thread, then spawns the test goroutine. This matches the production startup sequence exactly, making these tests true integration tests of the full rendering pipeline. - Separation: Build tags (
ci,no_glfw,migrated_fynedo) control which driver backend is compiled. On CI Ubuntu:ci,migrated_fynedotags are used, enabling GLFW tests with xvfb. On CI macOS:no_glfw,ci— GLFW tests are excluded, everything else runs. On Windows:no_glfw,migrated_fynedo— same headless approach. Mobile driver tests use!cigates to skip in the standard CI matrix, with a separatemobile_tests.ymlworkflow for Android/iOS. - Thread management pattern: GLFW tests require
TestMainto start the event loop on the main OS thread beforem.Run(). This is a known GLFW constraint (OpenGL contexts must be created on the main thread). Theinternal/driver/mobile/canvas_test.goandinternal/cache/base_test.goalso useTestMainfor initialization sequencing.
Test quality observations#
What’s done well#
- First-class test package exported to users.
fyne.io/fyne/v2/testis a stable, documented package that application developers use to test their own Fyne apps. The framework tests itself with the same tools it ships to users — no internal test privilege. - XML golden files over pixel-only. The markup snapshot format makes rendering regressions reviewable without image diffing tools. The
diff-failed.shscript andtestdata/failed/convention give developers a clear workflow for updating golden files. - Headless software painter eliminates GPU dependencies. The
driver/softwarepackage (backed byinternal/painter/software) renders entirely in CPU memory. Tests can run on any machine, including headless CI, without a GPU or display. The-raceflag is used in all CI test runs. - Platform tolerance in golden file comparison.
pixCloseEnoughprevents flaky failures from platform-specific rendering differences (Darwin/arm64 anti-aliasing) while still catching real regressions. - Build-tag test isolation. The same tag system used to select platform backends is used to exclude incompatible tests in CI. No test skips inside test functions (fragile) — exclusion happens at compile time.
TempWidgetRenderercleanup pattern (test/test_helper.go:148): registers at.Cleanupto destroy the widget renderer cache after each test, preventing cross-test pollution via the renderer cache singleton.
What could improve#
- Coverage floor is modest at 62%. Given that the
test/package provides a full headless rendering stack, higher coverage is plausible. The 62% floor reflects the reality that build-tag variants (mobile, Windows, WASM) are not covered by the primary Linux test run. - Table-driven tests are inconsistently adopted. Widget tests vary between function-per-scenario and table-driven style within the same file. A code review policy enforcing table-driven style for multi-case scenarios would improve consistency.
- Interaction helpers don’t model async behavior.
test.Tap,test.Type, etc. call widget methods synchronously. The one exception isTestButton_Tappedinwidget/button_test.gowhich usesgo test.Tap(button)with a channel and timeout — because the button fires its callback asynchronously. This pattern is not encapsulated in thetestpackage, leaving it to each test author. - No benchmarks visible in the main widget packages. Given that the refresh queue and cache are on hot paths, benchmarks for layout/render throughput would catch performance regressions.
Patterns worth emulating#
- Exporting a test helper package. Shipping
fyne.io/fyne/v2/testas a first-class library means widget authors never need to mock Fyne internals from scratch. Every framework or library that exposes interfaces should consider shipping a correspondingyourpkg/testpackage with canonical fakes. - XML markup snapshots for UI state. The
markupRenderer→.xmlgolden file pattern is far more maintainable than pixel PNG diffs for widget structure tests. It captures what matters (widget type, position, size, text content, fill colors) and produces human-readable diffs. The separate PNG golden files are reserved for pixel-accurate painter tests. - Build-tag based test exclusion over runtime skip. Using
//go:build !cito exclude GLFW-dependent tests on CI rather thant.Skip()at runtime keeps test output clean and avoids “skipped” noise in CI reports. The tradeoff is that the tag matrix must be understood and documented. TestMainfor thread-constrained tests. The GLFW test package usesTestMainto ensure the event loop is on the OS main thread before any test runs — the only correct solution for OpenGL-dependent tests. This pattern applies to any test suite that needs process-level setup (database connections, embedded servers, OS thread pinning).