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.yml fails the build if coverage drops below this threshold)

Test organization#

  • Placement: Both same-package and external _test package. In widget/ 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. Provides NewApp() (initializes a headless fyne.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 public test package; pixCloseEnough implements 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 .png golden images and .xml markup 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 in widget/richtext_test.go and widget/radio_group_internal_test.go for exhaustive input/output cases.
  • Style: Anonymous struct with a name field and typed fields/args/want decomposition:
    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 / AssertObjectRendersToImage across the test suite. This is the most heavily used assertion strategy.
  • How it works: The test.AssertRendersToMarkup function renders the canvas using the headless software painter, serializes the widget tree to XML (snapshot(c) via markupRenderer), then byte-compares against a stored .xml file in testdata/. On first run (no master), the generated file is written to testdata/failed/ for human review. A diff-failed.sh script exists in widget/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). The pixCloseEnough function in internal/test allows 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 of fyne.App, fyne.Driver, fyne.Canvas, fyne.Window, fyne.Clipboard, fyne.Preferences, fyne.Storage, and fyne.CloudProvider as concrete structs.
  • Example: test/app.go defines type app struct { driver *driver; settings *testSettings; ... } which satisfies fyne.App. test/driver.go defines type driver struct { painter SoftwarePainter; windows []fyne.Window; ... } which satisfies fyne.Driver. The fake driver’s DoFromGoroutine executes 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 with xvfb-run (virtual framebuffer) on Linux CI.
  • How: TestMain in internal/driver/glfw/window_test.go starts 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_fynedo tags 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 !ci gates to skip in the standard CI matrix, with a separate mobile_tests.yml workflow for Android/iOS.
  • Thread management pattern: GLFW tests require TestMain to start the event loop on the main OS thread before m.Run(). This is a known GLFW constraint (OpenGL contexts must be created on the main thread). The internal/driver/mobile/canvas_test.go and internal/cache/base_test.go also use TestMain for initialization sequencing.

Test quality observations#

What’s done well#

  • First-class test package exported to users. fyne.io/fyne/v2/test is 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.sh script and testdata/failed/ convention give developers a clear workflow for updating golden files.
  • Headless software painter eliminates GPU dependencies. The driver/software package (backed by internal/painter/software) renders entirely in CPU memory. Tests can run on any machine, including headless CI, without a GPU or display. The -race flag is used in all CI test runs.
  • Platform tolerance in golden file comparison. pixCloseEnough prevents 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.
  • TempWidgetRenderer cleanup pattern (test/test_helper.go:148): registers a t.Cleanup to 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 is TestButton_Tapped in widget/button_test.go which uses go test.Tap(button) with a channel and timeout — because the button fires its callback asynchronously. This pattern is not encapsulated in the test package, 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/test as 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 corresponding yourpkg/test package with canonical fakes.
  • XML markup snapshots for UI state. The markupRenderer.xml golden 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 !ci to exclude GLFW-dependent tests on CI rather than t.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.
  • TestMain for thread-constrained tests. The GLFW test package uses TestMain to 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).