gwt-tester
GWT Tester
Use these instructions to write clean, readable tests with Given/When/Then semantics.
Terminology Map
Use the agnostic terms below throughout this skill.
| Agnostic term | JS/TS example | Dart example |
|---|---|---|
| Suite block | describe |
group |
| Case block | it / test |
test |
| Setup once (for a suite) | before |
setUpAll |
| Setup each (per case) | beforeEach |
setUp |
| Teardown once (for a suite) | after |
tearDownAll |
| Teardown each (per case) | afterEach |
tearDown |
When this skill says:
- "suite block", read
describe(JS/TS) orgroup(Dart) - "case block", read
it/test(JS/TS) ortest(Dart) - "setup hook", read
before/beforeEach(JS/TS) orsetUpAll/setUp(Dart) - "teardown hook", read
after/afterEach(JS/TS) ortearDownAll/tearDown(Dart)
Rules
- Keep maximum nesting depth to 3 levels:
suite -> suite -> case. Exception: allowsuite -> suite -> suite -> caseforWhenvariant coverage when validating the same behavior across interfaces or variants (for examplevia RPCandvia REST). In this exception, variants are at level 3 andThencase blocks are at level 4. - Never nest
Givenblocks inside anotherGivenblock. - Keep
Givensetup hooks focused on shared context and test data, not primary behavior under test. - Prefer assertion-only case blocks.
- Move executable behavior into a dedicated
Whenblock whenever possible. Use setup hooks inside thatWhenblock to run the action once per case. If there is no executable action to perform for a scenario, skip theWhenblock and placeThencases directly under theGivenblock. If thatWhencontains variants, place action setup hooks inside each variant block, not in the parentWhenblock. - Ensure block titles describe behavior that actually happens in that block's scope. Do not title a case
When <action> ...if<action>already ran in another block's setup hook. - If a
Whenhas only oneThen, use a single case title (When ... then ...); in this collapsed form, putting theWhenaction inside the case body is acceptable. - Remove wrapper suite blocks that do not add scenario context.
- Write explicit scenario titles:
Given <subject> with <condition>, optionalWhen <action>, andThen <outcome>. - Place test utilities based on scope:
- If a utility is specific to one test file, place it at the bottom of that file so test blocks remain the first code in the file.
- If a utility is reused across test files, move it to shared test utilities following existing codebase conventions.
- If no shared utility convention exists, create an appropriate
utilsdirectory and place shared utilities there.
- Extract a value into a named variable only when that value is used more than once, and place it in the nearest shared scope (for example
GivenorWhen) to make test data updates predictable and centralized.
Writing Workflow
- Define scenarios as top-level
Given ...suite blocks. - Add
Givensetup hooks for shared context only. - Add nested
When ...suite blocks for each action path. If there is no action to perform, skipWhenand placeThencases directly underGiven. - Run the action in setup hooks inside the
Whenblock when multipleThencases share it. If variants exist under thatWhen, run setup hooks inside each variant block. - Add assertion-only
Then ...case blocks. - If a
Whenhas only oneThen, optionally collapse to a single case title:When ... then .... - Move logic from
Givenhooks intoWhenhooks when that logic is action-specific. - If a
Whenneeds interface/variant coverage, nest variant blocks under thatWhenonly (for examplevia RPC,via REST). - Keep nesting shallow and remove redundant wrappers.
- Run tests and formatter.
When Variant Coverage
Use one extra nesting level only for variant coverage under the same When action.
Keep action setup hooks inside each variant block, not in the parent When block.
Example 1 (setup inside variants):
suite("Given an endpoint that returns a user", () => {
suite("When requesting user 42", () => {
suite("via RPC", () => {
setup_each(() => {
response = requestUserViaRpc(42)
})
case("Then status is successful", () => {
assert(response.status == 200)
})
})
suite("via REST", () => {
setup_each(() => {
response = requestUserViaRest(42)
})
case("Then status is successful", () => {
assert(response.status == 200)
})
})
})
})
Example 2 (multiple Then outcomes per variant):
suite("Given a user endpoint", () => {
suite("When requesting user 42", () => {
suite("via RPC", () => {
setup_each(() => {
response = requestUserViaRpc(42)
})
case("Then status is successful", () => {
assert(response.status == 200)
})
case("Then user payload is present", () => {
assert(response.body.user != null)
})
})
suite("via REST", () => {
setup_each(() => {
response = requestUserViaRest(42)
})
case("Then status is successful", () => {
assert(response.status == 200)
})
case("Then user payload is present", () => {
assert(response.body.user != null)
})
})
})
})
Pseudocode Pattern
suite("Given <subject> with <state>", () => {
setup_each(() => {
// arrange shared context
})
suite("When <action>", () => {
setup_each(() => {
// act
})
case("Then <outcome A>", () => {
// assert only
})
case("Then <outcome B>", () => {
// assert only
})
})
})
No-action scenario (skip When):
suite("Given <subject> with <state>", () => {
setup_each(() => {
// arrange shared context only
})
case("Then <outcome A>", () => {
// assert only
})
})
Title-to-Scope Alignment
Use titles that match where behavior occurs.
Bad example:
suite("Given a counter at zero", () => {
setup_each(() => {
// Bad: action belongs in a When block, not Given setup.
counter.increment()
})
case("Then value is one", () => {
assert(counter.value == 1)
})
})
Good example:
suite("Given a counter at zero", () => {
setup_each(() => {
// arrange
})
suite("When incremented", () => {
setup_each(() => {
counter.increment()
})
case("Then value is one", () => {
assert(counter.value == 1)
})
case("Then value is not zero", () => {
assert(counter.value != 0)
})
})
})
Logic Movement for Clean Blocks
Move logic to the narrowest block that owns that behavior.
- Keep data creation and shared fixtures in
Givensetup hooks. - Move action-specific calls from
Givensetup hooks into the relevantWhenblock. - Keep
Thencase blocks assertion-only. - Only keep action execution in case bodies for a single
When ... then ...collapsed case.
When/Then Collapse
Apply collapse when one action has only one outcome assertion.
Bad example (not collapsed):
suite("Given a counter at zero", () => {
setup_each(() => {
// arrange
})
suite("When incremented", () => {
setup_each(() => {
counter.increment()
})
case("Then value is one", () => {
assert(counter.value == 1)
})
})
})
Good example (collapsed):
suite("Given a counter at zero", () => {
setup_each(() => {
// arrange
})
case("When incremented then value is one", () => {
counter.increment()
assert(counter.value == 1)
})
})
Utility Placement
Choose utility location by reuse scope.
- Keep file-specific utilities in the same test file, below all test blocks.
- Move cross-file utilities to shared test utility locations used by the codebase.
- If the repository has no established shared test utility location, create an appropriate
utilsdirectory for shared test helpers.
Reused Values
Extract repeated values into named variables near test setup only when the same value is used more than once.
Bad example (no variable extracted):
suite("Given a user lookup", () => {
suite("When searching by user id", () => {
case("Then it returns the matching user", () => {
result = findUser("user-42")
assert(result.id == "user-42")
})
case("Then it records an audit message for that user", () => {
assert(formatAuditMessage("user-42") == "audit:user-42")
})
})
})
Bad example (scope too broad):
suite("Given a user lookup", () => {
userId = "user-42" // Too broad: only shared inside the When block below.
suite("When searching by user id", () => {
case("Then it returns the matching user", () => {
result = findUser(userId)
assert(result.id == userId)
})
case("Then it records an audit message for that user", () => {
assert(formatAuditMessage(userId) == "audit:" + userId)
})
})
})
Good example:
suite("Given a user lookup", () => {
suite("When searching by user id", () => {
userId = "user-42" // Nearest shared scope.
case("Then it returns the matching user", () => {
result = findUser(userId)
assert(result.id == userId)
})
case("Then it records an audit message for that user", () => {
assert(formatAuditMessage(userId) == "audit:" + userId)
})
})
})
Refactoring Guide
When asked to refactor or check existing tests, evaluate against all rules, not only the rule that triggered the edit.
Use this pass order:
- Map current structure as
Given,When, andThenblocks. - Move action-specific logic out of
Givensetup hooks into the correctWhenblock. Keep scenarios with no action directly underGivenwithout adding an emptyWhen. - After each logic move, immediately re-evaluate collapse opportunities:
- If a
Whenblock now has only oneThen, collapse toWhen ... then .... - If additional outcomes still exist, keep
When+ multipleThencases.
- If a
- Re-check title-to-scope alignment after structural edits.
- Run the full review checklist before finishing.
Anti-Patterns
- Nesting
GiveninsideGiven. - Keeping action execution in
Givensetup when it can be moved to aWhenblock. - Calling the SUT inside a case block when it can be executed in
Whensetup hooks. - Mixing setup/action/asserts in a single case block.
- Adding wrapper suite blocks with no new context.
- Adding an empty
Whenblock when there is no action to perform. - Collapsing to
When ... then ...whenWhenvariants exist, even if there is only oneThenoutcome. - Depth greater than
suite -> suite -> case, exceptsuite -> suite -> suite -> caseforWhenvariants. - A title that claims an action runs in one block when it actually runs in another block.
Review Checklist
- Are all rules reviewed, not just the rule that prompted the refactor?
- Are there any nested
Givenblocks? - Does each case block contain assertions only?
- Does
Givensetup contain only shared context? - Is action logic moved into the relevant
Whenblock? - If there is no action to perform, is
Whenskipped and areThencases placed directly underGiven? - When variants exist, are action setup hooks placed inside each variant block instead of the parent
Whenblock? - After each logic move, was
When/Thencollapse re-evaluated? - Do suite and case titles match what actually happens in each block's scope?
- Are single
When+ singleThencases collapsed into one case block? - Are file-specific utilities placed at the bottom of the test file (with tests first)?
- Are shared utilities moved to shared locations that follow existing codebase patterns (or a new appropriate
utilsdirectory when no pattern exists)? - Are values extracted into named variables only when reused more than once, and placed in the nearest shared scope (for example
GivenorWhen)? - Is nesting depth <= 3, or
suite -> suite -> suite -> caseonly forWhenvariants (level 3) withThencase blocks at level 4 (for examplevia RPC/via REST)?