tdd
GitHub规范 Gum 项目行为变更的开发流程,强制要求先写失败的单元测试再修改代码。覆盖多平台测试目录映射,强调边界测试与手势规范,旨在通过 TDD 防止回归并提升代码质量。
Trigger Scenarios
Install
npx skills add vchelaru/Gum --skill tdd -g -y
SKILL.md
Frontmatter
{
"name": "tdd",
"description": "Test-first discipline for Gum. Triggers: behavior changes (bug fix or feature) under GumCommon\/, Gum\/, Tools\/Gum.Presentation\/, Tool\/ (the Avalonia head and the plugin cores), DataUi.Core\/, AvaloniaDataUi\/, MonoGameGum\/, RenderingLibrary\/, KniGum\/, FnaGum\/, SkiaGum\/, RaylibGum\/, Tools\/Gum.ProjectServices\/. Skip for docs, renames, csproj\/projitems plumbing, style-only edits."
}
Behavior changes require a failing test first
Behavior changes in Gum's source projects require a failing unit test in the matching test project before the source edit. Write it, run it via Bash, watch it fail for the right reason, then implement until it goes green.
Test projects to look in (pick the one that compiles the source you're editing):
Tests/Gum.ProjectServices.Tests/— forTools/Gum.ProjectServices/MonoGameGum.Tests/,Tests/MonoGameGum.Tests.V3/— forMonoGameGum/,GumCommon/,RenderingLibrary/Tests/SkiaGum.Tests/— forSkiaGum/Tests/Gum.Presentation.Tests/— forTools/Gum.Presentation/,DataUi.Core/, the plugin cores underTool/*.Core/, and the neutral plugins underGum/(the default for tool logic)Tests/Gum.Avalonia.Tests/— forTool/Gum.Avalonia/andAvaloniaDataUi/(headless Avalonia)Tool/Tests/GumToolUnitTests/— only for the frozen WPF head (Gum/,WpfDataUi/); new tool logic never lands thereTests/Gum.Cli.Tests/— forGum.Cli/Tests/Gum.Bundle.Tests/— forGumCommon/Bundle/(the dependency walker, font reference collector,.gumpkgformat)Tests/Gum.Themes.Tests/— forThemes/
No "the cause is obvious, I'll skip the test" exception — that reasoning is how silent regressions ship. If you're about to edit one of the directories above without a failing test open, stop.
Run the test yourself via Bash. A failure you only reasoned about is not a failure.
Exceptions: docs, csproj/projitems plumbing, pure renames, dead-code removal, cosmetic edits. When in doubt, write the test.
Note: extracting logic into a new class/service/ViewModel is not a pure rename. Even when the move preserves behavior, pin the new unit with a characterization test — see refactoring-direction. The exemption above is for renames and cosmetics, not for relocating logic into a newly-testable seam.
A gesture's spec is a table, agreed before the test
For an input gesture (a click, hotkey, drag or menu action), the failing test asserts an outcome table — gesture × state → where the result lands, which the user has confirmed — not the author's guess at the intent. A test that encodes a guess goes green on behavior the user never asked for; the Standards chip's Ctrl+click was specified as "add at the element root" and tested as such three times before the user's actual rule (every add gesture goes to the same add destination) was written down. When gestures are meant to be the same, they share one handler and one test, so a difference cannot exist.
Make it testable before you decide it can't
Before implementing, decide how the change will be proven — in this order:
- Can it be tested as-is? Then test it: cover the happy path, the negative cases (invalid input is rejected / the expected error is raised), and the edge/boundary cases (null, empty,
0,-1, first/last index, single-element collection, max). - If it's not testable as written, restructure until it is — even code you weren't otherwise here to change. "Can't be tested" is a reason to introduce a seam, not to skip the test. Extract the logic into a class/service that takes its dependencies via the constructor; if a static
.Selfsingleton blocks the seam, drain it on the spot (see CLAUDE.md "Static Singletons" + refactoring-direction for breaking the resulting DI cycle withLazy<T>). Plugin classes that callLocatordirectly are the canonical case: pull the logic into a ctor-injected service, test that, and leave only a thin untested plugin wrapper. - If it's a rendering change with nothing but drawn pixels to assert on (stroke thickness, blend color, clipping), a pixel test that renders offscreen and reads back is the proof, before falling back to manual. SkiaGum uses the golden-image harness in
Tests/SkiaGum.Tests/GoldenImages/(seegum-unit-tests); MonoGame usesTests/MonoGameGum.IntegrationTests/MonoGameGum/Rendering/; raylib usesTests/RaylibGum.Tests/Rendering/. - A tool gesture or wiring change (a click handler, a plugin event reaching a command) is testable end-to-end in
Tests/Gum.Avalonia.Tests:TestAppBuilder.Servicesis the head's real service graph, so a test can create a project, select throughISelectedState, invoke the control's action and assert on theElementSave— seeStandardsPaletteAddTests. Drive the control's action property rather than hosting the head's shared tab content in a test window; that content belongs to the main window in other tests. - Only if it genuinely can't be unit-tested, fall back to a manual visual/runtime check — and say so explicitly, with why. (The issue-driven workflow defines that manual step.)
Testability is a gate on the change, not a property you accept as given. Restructuring a blocker into a testable seam is in-scope work, not a separate task.
A new branch is a behavior change — cover it
A cache check, early-return, guard, or "while I'm here" optimization that alters control flow needs its own test — even when it's bolted onto already-working code and isn't the feature you set out to build. These are the classic blind spot: there's no ticket for them, so nobody asks "what covers this?", and a green-only test passes with or without the branch.
Red-first still applies: after adding a branch, remove or invert it and confirm a test goes red. If nothing fails, your change is uncovered — write the test that reaches it. (A real regression shipped exactly this way: a font-loader cache-hit early-return added alongside a feature, reached by no test because every existing font test disabled caching.)
Apply this to the whole diff, not just the branch you set out to add — a refactor that changes how an existing path works (swapping an index lookup for a reference lookup, rerouting a removal) is a changed branch too, and "the full suite is green" only proves the paths the suite already exercised. Before committing, name any branch your diff touched that no test would catch regressing; scoping coverage to the bugs that had a crash-repro is exactly how an untested rework slips through.
Writing the tests
- Quality over coverage. The fewest tests that meaningfully cover the change — 1 ideally, 2–3 only when the feature has genuinely distinct cases. Don't ship near-duplicate tests; combine them or keep the representative one.
- Self-contained arrangements. Every value a test asserts against must be declared in that test's own Arrange section. Shared helpers may do common setup (file creation, object init) but must take the asserted values as parameters — never let a helper define an expected value, or the test breaks silently when the helper changes.
Version History
-
7fc2261
Current 2026-09-28 09:48
更新 macOS 集成测试支持,增加 GPU 像素测试指导,明确 MonoGame 和 Raylib 测试框架映射。
-
78a2f53
2026-09-22 22:53
新增 AvaloniaDataUi 等项目的测试路径映射;强调手势规格需以表格形式确认;细化重构时的特征测试要求;补充边界情况测试指引。
- c93866f 2026-08-20 09:20


