initial commit
This commit is contained in:
commit
2346cf156b
2
.claude/git/.gitignore
vendored
Normal file
2
.claude/git/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
2
.claude/npm/.gitignore
vendored
Normal file
2
.claude/npm/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
4
.claude/settings.json
Normal file
4
.claude/settings.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"packages": [
|
||||||
|
]
|
||||||
|
}
|
||||||
24
.claude/skills/task-breakdown/SKILL.md
Normal file
24
.claude/skills/task-breakdown/SKILL.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: task-breakdown
|
||||||
|
description: "Break down complex tasks into test lists Use when: planning work, decomposing features, tracking task progress, or organizing multi-step implementations. Attach file locations and implementation details as multi line comment after test description."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Task Breakdown & Progress Tracking
|
||||||
|
|
||||||
|
This skill provides the workflow for decomposing complex tasks into tracked subtasks and managing their lifecycle using simple test lists
|
||||||
|
|
||||||
|
## Core Principle
|
||||||
|
|
||||||
|
**Always write a list of test names as individual comments in the test file**
|
||||||
|
|
||||||
|
The executing agent will use this list to implement
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### 1. Break Down a Task
|
||||||
|
|
||||||
|
Ask the user clarifying questions. One question at a time. Make a test list with the user. Each test should fail.
|
||||||
|
|
||||||
|
Ask the user clarifications about the domain concepts and api. Write the decision in the test file as well
|
||||||
|
|
||||||
|
Add the minimal amount of functionality, only what the test requires.
|
||||||
34
.claude/skills/task-execution/SKILL.md
Normal file
34
.claude/skills/task-execution/SKILL.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
name: task-execution
|
||||||
|
description: "Execute pre-broken-down tasks using the test list. Use when: inheriting a task list, picking the next task to work on, reading task context before implementation, or discovering new subtasks during implementation."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Task Execution & Progress Tracking
|
||||||
|
|
||||||
|
This skill provides the workflow for **executing** tasks that have already been broken down by the `task-breakdown` skill. It complements TDD workflows by keeping task state in sync with implementation progress.
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
**Always use test list**
|
||||||
|
|
||||||
|
Turn the comment into the name of the Test, delete the comment. Delete the sample implementation, use it as inspiration, but beware that details may have changed.
|
||||||
|
|
||||||
|
**Work together with user**
|
||||||
|
|
||||||
|
- Ask user to review when tests are green before committing.
|
||||||
|
|
||||||
|
- Ask the user clarifiying questions when in doubt or when the test passes in the red phase.
|
||||||
|
|
||||||
|
- Ask user permission to proceed to the next test
|
||||||
|
|
||||||
|
## Relationship to Other Skills
|
||||||
|
|
||||||
|
| Skill | Role |
|
||||||
|
|-------|------|
|
||||||
|
| `task-breakdown` | Decomposes features into tracked subtasks (planning phase) |
|
||||||
|
| `task-execution` | Executes pre-broken-down tasks (implementation phase) |
|
||||||
|
| `tdd-minimalism` | Ensures minimal implementations during the Red-Green-Refactor cycle |
|
||||||
|
| `test-focus-isolation` | Ensures each test fails for only one reason |
|
||||||
|
| `testcase-common-setup` | Reduces test duplication via fixtures |
|
||||||
|
|
||||||
|
Use `task-breakdown` first to plan, then `task-execution` to implement, with `tdd-minimalism` and test skills guiding the actual coding.
|
||||||
139
.claude/skills/tdd-minimalism/SKILL.md
Normal file
139
.claude/skills/tdd-minimalism/SKILL.md
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
---
|
||||||
|
name: tdd-minimalism
|
||||||
|
user-invocable: true
|
||||||
|
description: "Use when: implementing code to pass tests, writing minimal implementations, TDD workflow. Enforces strict minimalism by analyzing tests first and generating only the absolute minimum code needed. Prevents overengineering and anticipatory code. Run this skill before implementation on any failing test."
|
||||||
|
---
|
||||||
|
|
||||||
|
# TDD Minimalism Guardrails
|
||||||
|
|
||||||
|
This skill enforces strict Test-Driven Development discipline by ensuring implementations contain **only** the code that tests require—nothing more.
|
||||||
|
|
||||||
|
## Principle
|
||||||
|
|
||||||
|
Generate **only the absolute minimum** needed to pass tests. Anticipatory code, infrastructure for future scenarios, defensive checks, comments explaining future work, or over-engineered data structures all violate TDD and should be deleted before writing implementation code.
|
||||||
|
|
||||||
|
## Pre-Implementation Workflow
|
||||||
|
|
||||||
|
Before writing implementation code, ALWAYS execute this checklist:
|
||||||
|
|
||||||
|
### 1. Analyze the Test
|
||||||
|
|
||||||
|
- Read the test completely
|
||||||
|
- Identify exactly what methods/functions the test **calls**
|
||||||
|
- Identify exactly what the test **asserts**
|
||||||
|
- Document nothing else matters for this test
|
||||||
|
|
||||||
|
### 2. Define Minimal State
|
||||||
|
|
||||||
|
Ask: "What data must persist across the test's method calls to satisfy the assertions?"
|
||||||
|
|
||||||
|
- Only that data gets stored as member variables
|
||||||
|
- Everything else is deleted before writing code
|
||||||
|
|
||||||
|
### 3. Implement Only Tested Paths
|
||||||
|
|
||||||
|
- Implement **only** the specific code paths the test exercises
|
||||||
|
- **No conditional logic** unless the test verifies multiple branches
|
||||||
|
- **No utility methods or helper classes**
|
||||||
|
- **No defensive code** (null checks, boundary conditions the test doesn't trigger)
|
||||||
|
- **No comments explaining future features**
|
||||||
|
- **No includes** you don't use
|
||||||
|
|
||||||
|
### 4. Question Every Line
|
||||||
|
|
||||||
|
For each piece of code, ask: "Does this test directly exercise this line?"
|
||||||
|
|
||||||
|
- **No** → Delete it immediately
|
||||||
|
- **Yes** → Keep it
|
||||||
|
|
||||||
|
## Anti-Patterns to Eliminate
|
||||||
|
|
||||||
|
| ❌ Anti-Pattern | Why It's Wrong | ✅ Corrected |
|
||||||
|
| ---------------------------------------------------------- | ---------------------------------------- | -------------------------- |
|
||||||
|
| Store data in a map/vector when test uses only 1 item | Implements multi-item support not tested | Use single member variable |
|
||||||
|
| Add `if` checks for untested scenarios | Defensive code the test doesn't exercise | Return value directly |
|
||||||
|
| Include `#include <map>` when using `int` | Unused dependency | Remove it |
|
||||||
|
| Define `lastChoice` member when `select()` doesn't read it | Dead code | Remove unused members |
|
||||||
|
| Comment explaining "future work" or "TODO" | Out of scope for current test | No implementation comments |
|
||||||
|
| Use compound assignment (`+=`, `-=`) when test calls method only once | Anticipates future multi-call scenarios not covered by any test | Use simple assignment (`=`) instead |
|
||||||
|
| Create utility methods not called in test | Anticipatory code | Delete before commit |
|
||||||
|
|
||||||
|
## Red-Green-Refactor Phases
|
||||||
|
|
||||||
|
Each phase **must** include running the test suite to confirm the expected outcome. The test command is `./run_tests.sh` (or the project's equivalent).
|
||||||
|
|
||||||
|
### Red Phase (Writing Tests)
|
||||||
|
|
||||||
|
1. Write a test describing one specific behavior
|
||||||
|
|
||||||
|
2. **Run the test suite** (`./run_tests.sh`) — confirm the test **fails** (either fails to compile or fails at runtime on an assertion).
|
||||||
|
- **If the test passes**: The behavior is already covered by existing code. **STOP. Abort and discuss with the user.** Options include: the test is redundant (delete it), the test name/assertions are wrong, or the requirement changed. Do NOT proceed without user input.
|
||||||
|
- **If the test fails to compile**: Write the absolute minimum stubs (empty classes, empty method signatures) needed to make the code compile, then **run the test suite again** — confirm the test now **fails at runtime** on the assertion (`EXPECT_*`/`ASSERT_*`), not just on compilation. This validates the assertion logic is correct before implementing.
|
||||||
|
- **If the test fails at runtime**: You may proceed to the Green phase.
|
||||||
|
|
||||||
|
3. Keep scope narrow (single scenario, not edge cases)
|
||||||
|
4. Test only what's currently required by the story
|
||||||
|
|
||||||
|
### Green Phase (Implementation)
|
||||||
|
|
||||||
|
1. Implement **only** what the test directly calls
|
||||||
|
2. Use the simplest possible data types
|
||||||
|
3. Use the shortest possible logic
|
||||||
|
4. **Run the test suite** (`./run_tests.sh`) — confirm it **passes**
|
||||||
|
5. Reference this skill's anti-patterns table and eliminate violations before committing
|
||||||
|
|
||||||
|
6. **Present the implementation to the user for review** before committing or proceeding to the next test.
|
||||||
|
- Show the final state of all changed files (diff or summary)
|
||||||
|
- Ask explicitly: "Please review and approve before I proceed"
|
||||||
|
- Do NOT move to the next test, commit, or merge without explicit user approval
|
||||||
|
- If the user requests changes, make them and re-run the test suite before asking for review again
|
||||||
|
|
||||||
|
### Refactor Phase
|
||||||
|
|
||||||
|
1. Improve code clarity (rename variables, format nicely)
|
||||||
|
2. **Run the test suite** (`./run_tests.sh`) — confirm tests still **pass**
|
||||||
|
3. **Never** add functionality without a failing test first
|
||||||
|
4. **Never** add infrastructure for "future" requirements
|
||||||
|
|
||||||
|
## Post-Implementation Verification
|
||||||
|
|
||||||
|
After implementing and tests pass, run this verification:
|
||||||
|
|
||||||
|
- [ ] Can I remove any member variables? (Are they all accessed in at least one method?)
|
||||||
|
- [ ] Can I remove any `#include` directives? (Do I use everything I include?)
|
||||||
|
- [ ] Can I remove any conditional branches? (Does the test exercise all paths?)
|
||||||
|
- [ ] Can I remove any comments? (Are they implementation comments, not documentation?)
|
||||||
|
- [ ] Can I remove any method parameters? (Are they all used in the method?)
|
||||||
|
- [ ] Can I replace any compound assignment (`+=`, `-=`, etc.) with simple assignment (`=`) without changing test outcomes?
|
||||||
|
|
||||||
|
If you answer **yes** to any question, make those deletions immediately. Do not proceed to the next test/feature without eliminating unnecessary code.
|
||||||
|
|
||||||
|
## Example: The Vending Machine
|
||||||
|
|
||||||
|
**Test** (what it calls and asserts):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
machine.addChoice(Choice::Cola, 0); // Calls this
|
||||||
|
machine.assignProduct(Choice::Cola, Brand::Coke); // Calls this
|
||||||
|
EXPECT_EQ(machine.select(Choice::Cola), Brand::Coke); // Asserts this
|
||||||
|
```
|
||||||
|
|
||||||
|
**Minimal Implementation:**
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Header: ONE member variable (lastBrand), because select() returns it
|
||||||
|
Brand lastBrand;
|
||||||
|
|
||||||
|
// Implementation
|
||||||
|
void addChoice(Choice, int) {} // Body empty; test calls but doesn't verify behavior
|
||||||
|
void assignProduct(Choice choice, Brand brand) { lastBrand = brand; }
|
||||||
|
std::optional<Brand> select(Choice choice) { return lastBrand; } // No if; test only selects once
|
||||||
|
```
|
||||||
|
|
||||||
|
**Eliminated Code:**
|
||||||
|
|
||||||
|
- ❌ `#include <map>` — Not using maps
|
||||||
|
- ❌ `std::map<Choice, Brand> productMap` — Test uses only one assignment
|
||||||
|
- ❌ `if (it != productMap.end())` — Test doesn't verify "choice not found" scenario
|
||||||
|
- ❌ Comments explaining future work — Out of test scope
|
||||||
|
- ❌ `Choice lastChoice` — `select()` doesn't read it; never used
|
||||||
124
.claude/skills/tdd-test-refactoring/SKILL.md
Normal file
124
.claude/skills/tdd-test-refactoring/SKILL.md
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
---
|
||||||
|
name: tdd-test-refactoring
|
||||||
|
user-invocable: true
|
||||||
|
description: "Use during Green/Refactor phase to eliminate test duplication. Extracts shared setup into TEST_F fixtures, removes redundant expectations, and keeps tests minimal and focused. Run after tests pass but before committing."
|
||||||
|
---
|
||||||
|
|
||||||
|
# TDD Test Refactoring Patterns
|
||||||
|
|
||||||
|
This skill complements **tdd-minimalism** by focusing on **test code quality** during the Green and Refactor phases. After implementation is minimal and passing, apply these patterns to keep the test suite clean.
|
||||||
|
|
||||||
|
## Principle
|
||||||
|
|
||||||
|
Tests are code too — they accumulate duplication and cruft just like implementation. Each test should test **one distinct scenario** without repeating setup or expectations already covered by other tests.
|
||||||
|
|
||||||
|
## Refactoring Checklist
|
||||||
|
|
||||||
|
After tests pass (Green phase), review the test file for these patterns:
|
||||||
|
|
||||||
|
### 1. Extract Shared Setup into TEST_F
|
||||||
|
|
||||||
|
If multiple tests construct the same object(s) with the same configuration, extract the common setup into a fixture.
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```cpp
|
||||||
|
TEST(VendingMachineTest, configured_machine_delivers_can) {
|
||||||
|
VendingMachine machine;
|
||||||
|
machine.Configure(Choice::Cola, Can::Coke);
|
||||||
|
EXPECT_EQ(machine.Deliver(Choice::Cola), Can::Coke);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(VendingMachineTest, multiple_choices_deliver_different_cans) {
|
||||||
|
VendingMachine machine;
|
||||||
|
machine.Configure(Choice::Cola, Can::Coke);
|
||||||
|
machine.Configure(Choice::Fanta, Can::Orange);
|
||||||
|
EXPECT_EQ(machine.Deliver(Choice::Cola), Can::Coke);
|
||||||
|
EXPECT_EQ(machine.Deliver(Choice::Fanta), Can::Orange);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```cpp
|
||||||
|
class VendingMachineTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
machine.Configure(Choice::Cola, Can::Coke);
|
||||||
|
}
|
||||||
|
VendingMachine machine;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(VendingMachineTest, configured_machine_delivers_can) {
|
||||||
|
EXPECT_EQ(machine.Deliver(Choice::Cola), Can::Coke);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(VendingMachineTest, multiple_choices_deliver_different_cans) {
|
||||||
|
machine.Configure(Choice::Fanta, Can::Orange);
|
||||||
|
EXPECT_EQ(machine.Deliver(Choice::Cola), Can::Coke);
|
||||||
|
EXPECT_EQ(machine.Deliver(Choice::Fanta), Can::Orange);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to keep TEST instead of TEST_F:**
|
||||||
|
- The test needs a **different** or **empty** initial state (e.g., an unconfigured machine)
|
||||||
|
- The test's setup is entirely unique and not shared with any other test
|
||||||
|
|
||||||
|
### 2. Remove Redundant Expectations
|
||||||
|
|
||||||
|
If a test is a **strict superset** of another test (covers the same scenario plus more), the subset test may be redundant.
|
||||||
|
|
||||||
|
**Check:** Does the superset test already assert everything the subset test asserts?
|
||||||
|
|
||||||
|
| Scenario | Keep? |
|
||||||
|
|----------|-------|
|
||||||
|
| Superset test covers all assertions of subset | Consider removing subset if it adds no unique coverage |
|
||||||
|
| Superset test covers subset's assertions **plus** additional ones | Keep both if subset documents a specific edge case clearly |
|
||||||
|
| Tests cover different branches of the same logic | Keep both — they test different paths |
|
||||||
|
|
||||||
|
### 3. Name Tests by Scenario, Not by Order
|
||||||
|
|
||||||
|
Test names should describe **what scenario** they cover, not their sequence number.
|
||||||
|
|
||||||
|
| ❌ Bad | ✅ Good |
|
||||||
|
|--------|--------|
|
||||||
|
| `test1`, `test2`, `test3` | `choiceless_machine_deliver_nothing` |
|
||||||
|
| `first_test`, `second_test` | `configured_machine_delivers_can` |
|
||||||
|
| `test_feature_x` | `multiple_choices_deliver_different_cans` |
|
||||||
|
|
||||||
|
### 4. One Logical Assertion Per Test
|
||||||
|
|
||||||
|
Each test should verify **one behavior**. Avoid asserting unrelated things in the same test.
|
||||||
|
|
||||||
|
**When multiple assertions are OK:**
|
||||||
|
- They verify the **same result** from different angles (e.g., `.has_value()` and `== expected`)
|
||||||
|
- They verify a **post-condition** that naturally produces multiple observable effects
|
||||||
|
|
||||||
|
**When to split:**
|
||||||
|
- Assertions exercise **different code paths** or **different methods**
|
||||||
|
- One assertion failing hides whether the other would also fail
|
||||||
|
|
||||||
|
### 5. Keep Tests Independent
|
||||||
|
|
||||||
|
Tests must not depend on each other's side effects. Each test (or each fixture instance) starts fresh.
|
||||||
|
|
||||||
|
- ✅ Each `TEST_F` gets a **fresh** `SetUp()`
|
||||||
|
- ✅ No shared mutable state between tests
|
||||||
|
- ❌ No test ordering dependencies
|
||||||
|
|
||||||
|
## Post-Refactoring Verification
|
||||||
|
|
||||||
|
After applying test refactoring:
|
||||||
|
|
||||||
|
- [ ] Run the test suite — all tests still pass
|
||||||
|
- [ ] Run clang-format — all files comply
|
||||||
|
- [ ] No test is a complete subset of another (unless documenting a specific edge case)
|
||||||
|
- [ ] No duplicate setup code across tests using the same initial state
|
||||||
|
- [ ] Test names describe the scenario, not the sequence
|
||||||
|
|
||||||
|
## Relationship to tdd-minimalism
|
||||||
|
|
||||||
|
| Skill | Focus | When |
|
||||||
|
|-------|-------|------|
|
||||||
|
| tdd-minimalism | Implementation code — eliminate overengineering | Before writing implementation |
|
||||||
|
| tdd-test-refactoring | Test code — eliminate duplication and cruft | After tests pass, before commit |
|
||||||
|
|
||||||
|
Run **tdd-minimalism** before implementation. Run **tdd-test-refactoring** after tests pass but before committing.
|
||||||
80
.claude/skills/test-focus-isolation/SKILL.md
Normal file
80
.claude/skills/test-focus-isolation/SKILL.md
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
name: test-focus-isolation
|
||||||
|
description: 'Refactor tests so each one fails for only one reason and asserts one conceptual behavior. Use when: reviewing/refactoring tests with overlapping assertions, duplicate checks, or tests that verify the same behavior as another test.'
|
||||||
|
---
|
||||||
|
|
||||||
|
# Test Focus Isolation
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
- Two or more tests assert the same outcome (e.g., both check Cola delivery)
|
||||||
|
- A test mixes assertions about different conceptual behaviors
|
||||||
|
- A failure could break multiple tests for the same root cause
|
||||||
|
- Reviewing test coverage and finding overlapping checks
|
||||||
|
|
||||||
|
## Principles
|
||||||
|
|
||||||
|
### 1. One Reason to Fail Per Test
|
||||||
|
|
||||||
|
Each test should fail for **exactly one root cause**. If the same assertion appears in multiple tests, a single bug breaks multiple tests — this wastes debugging time and reduces signal quality.
|
||||||
|
|
||||||
|
**Rule**: A specific behavior or assertion should appear in exactly **one** test.
|
||||||
|
|
||||||
|
**✗ Bad** — Both tests fail if Cola delivery breaks:
|
||||||
|
```cpp
|
||||||
|
TEST_F(suite, cola_delivery) {
|
||||||
|
EXPECT_EQ(vm.deliver(Choice::Cola), Brand::Coke);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(suite, multiple_choices) {
|
||||||
|
EXPECT_EQ(vm.deliver(Choice::Cola), Brand::Coke); // ← duplicate
|
||||||
|
EXPECT_EQ(vm.deliver(Choice::FizzyOrange), Brand::Fanta);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**✓ Good** — Each behavior is tested once:
|
||||||
|
```cpp
|
||||||
|
TEST_F(suite, cola_delivery) {
|
||||||
|
EXPECT_EQ(vm.deliver(Choice::Cola), Brand::Coke);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(suite, multiple_choices) {
|
||||||
|
// Only the NEW behavior this test introduces
|
||||||
|
EXPECT_EQ(vm.deliver(Choice::FizzyOrange), Brand::Fanta);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. One Conceptual Assert Per Test
|
||||||
|
|
||||||
|
A test should verify **one conceptual behavior**. If a test checks two different things (e.g., Cola delivery AND FizzyOrange delivery), split it into two tests — unless the concept being tested is specifically "multiple choices work together."
|
||||||
|
|
||||||
|
**Rule**: Each test name should answer "yes/no" to a single question. The assertions inside should all serve that same question.
|
||||||
|
|
||||||
|
**✗ Bad** — Two behaviors in one test:
|
||||||
|
```cpp
|
||||||
|
TEST_F(suite, processes_orders) {
|
||||||
|
EXPECT_EQ(vm.deliver(Choice::Cola), Brand::Coke);
|
||||||
|
EXPECT_EQ(vm.deliver(Choice::FizzyOrange), Brand::Fanta);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**✓ Good** — Each test asks one question:
|
||||||
|
```cpp
|
||||||
|
TEST_F(suite, delivers_cola) {
|
||||||
|
EXPECT_EQ(vm.deliver(Choice::Cola), Brand::Coke);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(suite, delivers_fizzy_orange) {
|
||||||
|
EXPECT_EQ(vm.deliver(Choice::FizzyOrange), Brand::Fanta);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Exception**: If the concept being tested inherently involves multiple values (e.g., "supports multiple choices simultaneously"), it's acceptable to check all of them — because the test name communicates the multi-behavior concept.
|
||||||
|
|
||||||
|
## Procedure
|
||||||
|
|
||||||
|
1. **Scan tests for overlapping assertions** — grep for repeated `EXPECT_EQ`, `EXPECT_TRUE`, etc. across tests in the same suite.
|
||||||
|
2. **Identify the unique behavior** each test introduces — what does this test verify that no other test verifies?
|
||||||
|
3. **Remove duplicate assertions** — keep the assertion only in the test that best owns it. Delete it from the others.
|
||||||
|
4. **Rename tests** to clearly describe their singular behavior (e.g., `delivers_cola` instead of `configured_choice_delivers_correct_can`).
|
||||||
|
5. **Split tests** if a single test asserts multiple unrelated behaviors (unless the concept is multi-behavior by nature).
|
||||||
|
6. **Verify** — run the test suite to ensure coverage is preserved and tests pass.
|
||||||
161
.claude/skills/testcase-common-setup/SKILL.md
Normal file
161
.claude/skills/testcase-common-setup/SKILL.md
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
---
|
||||||
|
name: testcase-common-setup
|
||||||
|
description: 'Use when: refactoring tests to eliminate duplication via test fixtures, extracting common setup into SetUp(), converting TEST() to TEST_F() in Google Test. Run this after adding new test cases that duplicate setup code.'
|
||||||
|
user-invocable: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Test Case Common Setup (Google Test)
|
||||||
|
|
||||||
|
This skill refactors Google Test suites by extracting duplicate setup code into a shared test fixture with `SetUp()`. Applying this skill makes tests more maintainable and reduces noise from repeated scaffolding.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- Multiple `TEST()` cases create the same object or call the same configuration
|
||||||
|
- A new test case requires copying setup lines from an existing test
|
||||||
|
- Reviewing test code and noticing the same 2+ lines repeated across every test
|
||||||
|
- After adding a new test that duplicates setup from existing tests
|
||||||
|
|
||||||
|
## Procedure
|
||||||
|
|
||||||
|
### 1. Identify Duplicated Setup
|
||||||
|
|
||||||
|
Scan all `TEST()` blocks in the test suite. Look for lines that are **identical** across every test — typically:
|
||||||
|
|
||||||
|
- Object creation: `VendingMachine vm;` (or similar SUT construction)
|
||||||
|
- Common configuration calls: `vm.configureSlot(...)`
|
||||||
|
- Pre-conditions that apply to all tests
|
||||||
|
|
||||||
|
### 2. Create a Test Fixture
|
||||||
|
|
||||||
|
Introduce a fixture class deriving from `::testing::Test`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class <TestSuiteName> : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
// Shared state goes here — typically the System Under Test
|
||||||
|
<Type> <variable>;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- Place it right after the includes, before the first test
|
||||||
|
- Declare shared variables as `protected` members
|
||||||
|
- Name the fixture after the test suite (e.g. `VendingMachineTest`)
|
||||||
|
|
||||||
|
### 3. Extract Shared Setup into `SetUp()`
|
||||||
|
|
||||||
|
Define `void SetUp() override` inside the fixture with the lines common to **all** tests:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class <TestSuiteName> : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
<Type> <variable>;
|
||||||
|
|
||||||
|
void SetUp() override
|
||||||
|
{
|
||||||
|
// Lines that every test needs — extracted here once
|
||||||
|
<sharedSetupLine1>;
|
||||||
|
<sharedSetupLine2>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Only move lines that appear in **every** test
|
||||||
|
- If a line only appears in some tests, leave it in the individual `TEST_F()` bodies
|
||||||
|
- Keep `SetUp()` focused — it runs before each test, so don't put expensive or test-specific logic there
|
||||||
|
|
||||||
|
### 4. Convert `TEST()` to `TEST_F()`
|
||||||
|
|
||||||
|
Replace every occurrence:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Before
|
||||||
|
TEST(<TestSuiteName>, <TestCaseName>)
|
||||||
|
{
|
||||||
|
<Type> <variable>; // ← now redundant
|
||||||
|
<sharedSetupLine1>; // ← now redundant (in SetUp)
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
// After
|
||||||
|
TEST_F(<TestSuiteName>, <TestCaseName>)
|
||||||
|
{
|
||||||
|
// Only test-specific code remains
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Remove the redundant object declaration from each test body
|
||||||
|
- Remove any lines now handled by `SetUp()`
|
||||||
|
- Remove configuration calls that were duplicates across all tests
|
||||||
|
|
||||||
|
### 5. Verify
|
||||||
|
|
||||||
|
Run the test suite to confirm all tests still pass:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./run_tests.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
All tests must pass before considering the refactor complete.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Before — three tests with duplicated setup:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
TEST(VendingMachineTest, unconfigured_choice_dispenses_nothing)
|
||||||
|
{
|
||||||
|
VendingMachine vm;
|
||||||
|
EXPECT_EQ(vm.dispense(Choice::Cola), std::nullopt);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(VendingMachineTest, configured_slot_dispenses_can)
|
||||||
|
{
|
||||||
|
VendingMachine vm;
|
||||||
|
vm.configureSlot(Choice::Cola, Can::Coke);
|
||||||
|
EXPECT_EQ(vm.dispense(Choice::Cola), Can::Coke);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(VendingMachineTest, multiple_slots_dispense_correctly)
|
||||||
|
{
|
||||||
|
VendingMachine vm;
|
||||||
|
vm.configureSlot(Choice::FizzyOrange, Can::Fanta);
|
||||||
|
vm.configureSlot(Choice::Cola, Can::Coke);
|
||||||
|
EXPECT_EQ(vm.dispense(Choice::FizzyOrange), Can::Fanta);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After — fixture with `SetUp()`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class VendingMachineTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
VendingMachine vm;
|
||||||
|
|
||||||
|
void SetUp() override
|
||||||
|
{
|
||||||
|
vm.configureSlot(Choice::Cola, Can::Coke);
|
||||||
|
vm.configureSlot(Choice::FizzyOrange, Can::Fanta);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(VendingMachineTest, unconfigured_choice_dispenses_nothing)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(vm.dispense(Choice::Beer), std::nullopt);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(VendingMachineTest, configured_slot_dispenses_can)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(vm.dispense(Choice::Cola), Can::Coke);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(VendingMachineTest, multiple_slots_dispense_correctly)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(vm.dispense(Choice::FizzyOrange), Can::Fanta);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Google Test: Test Fixtures](https://google.github.io/googletest/primer.html#same-data-multiple-tests)
|
||||||
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.git
|
||||||
|
**/bin
|
||||||
|
**/obj
|
||||||
|
**/target
|
||||||
|
.idea/
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
build.sh
|
||||||
|
run-local.sh
|
||||||
|
donstro/
|
||||||
|
README.md
|
||||||
|
rebuild.sh
|
||||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
bin
|
||||||
|
packages
|
||||||
|
obj
|
||||||
|
*.userprefs
|
||||||
|
.idea
|
||||||
|
VendingMachine.sln.DotSettings.user
|
||||||
|
donstro/remote.secrets
|
||||||
|
|
||||||
1
.pi/skills
Symbolic link
1
.pi/skills
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../.github/skills/
|
||||||
8
Dockerfile
Normal file
8
Dockerfile
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
FROM 525595969507.dkr.ecr.eu-central-1.amazonaws.com/qwan/csharp:latest
|
||||||
|
|
||||||
|
COPY --chown=experience:experience ./ vendingmachine
|
||||||
|
RUN cd vendingmachine &&\
|
||||||
|
git init . && git add . && git commit -am "initial commit" &&\
|
||||||
|
git remote add qwan /git/qwan/vendingmachine
|
||||||
|
|
||||||
|
USER experience
|
||||||
57
README.md
Normal file
57
README.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# Vending Machine C# - Test Driven Development exercise
|
||||||
|
|
||||||
|
## Building the Docker image
|
||||||
|
|
||||||
|
To build the course image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
To push it to AWS registry:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-login.sh
|
||||||
|
./build.sh push
|
||||||
|
```
|
||||||
|
|
||||||
|
## Set up for deployment
|
||||||
|
|
||||||
|
Add an SSH config for `course-server`:
|
||||||
|
```
|
||||||
|
Host course-server
|
||||||
|
Hostname ec2-3-65-110-13.eu-central-1.compute.amazonaws.com
|
||||||
|
IdentityFile ~/.ssh/<your key goes here>
|
||||||
|
User ubuntu
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Deployment uses Donstro: https://github.com/rwestgeest/donstro
|
||||||
|
|
||||||
|
The docker-compose file describing the course instances can be found in `donstro/`
|
||||||
|
|
||||||
|
Deploy the composition to `course-server` using Donstro, first let the server login to AWS ECR (using `docker-login` from the course-infra repo):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-login.sh course-server
|
||||||
|
```
|
||||||
|
|
||||||
|
Then deploy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
donstro_single up
|
||||||
|
```
|
||||||
|
|
||||||
|
## stopping containers
|
||||||
|
donstro_single stop
|
||||||
|
|
||||||
|
## getting in a container
|
||||||
|
donstro_single exec cat bash
|
||||||
|
|
||||||
|
## cleanup volumes
|
||||||
|
donstro_single down -v
|
||||||
|
|
||||||
|
## Setting login passwords for the containers
|
||||||
|
|
||||||
|
TBD
|
||||||
11
Vending.Tests/TestVendingMachine.cs
Normal file
11
Vending.Tests/TestVendingMachine.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace Vending.Tests;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class TestVendingMachine
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void ItFails()
|
||||||
|
{
|
||||||
|
Assert.That(0, Is.EqualTo(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
28
Vending.Tests/Vending.Tests.csproj
Normal file
28
Vending.Tests/Vending.Tests.csproj
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
|
<PackageReference Include="NUnit.Analyzers" Version="3.9.0" />
|
||||||
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="NUnit.Framework" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Vending\Vending.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
2
Vending/Program.cs
Normal file
2
Vending/Program.cs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// See https://aka.ms/new-console-template for more information
|
||||||
|
Console.WriteLine("Hello, World!");
|
||||||
10
Vending/Vending.csproj
Normal file
10
Vending/Vending.csproj
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
28
VendingMachine.sln
Normal file
28
VendingMachine.sln
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vending", "Vending\Vending.csproj", "{CFC6ADCF-20F2-4E9E-82E5-ABBF9F5FDB58}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vending.Tests", "Vending.Tests\Vending.Tests.csproj", "{B041B730-AEEA-4A78-B605-19C36563402C}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{CFC6ADCF-20F2-4E9E-82E5-ABBF9F5FDB58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{CFC6ADCF-20F2-4E9E-82E5-ABBF9F5FDB58}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{CFC6ADCF-20F2-4E9E-82E5-ABBF9F5FDB58}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{CFC6ADCF-20F2-4E9E-82E5-ABBF9F5FDB58}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B041B730-AEEA-4A78-B605-19C36563402C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B041B730-AEEA-4A78-B605-19C36563402C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B041B730-AEEA-4A78-B605-19C36563402C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B041B730-AEEA-4A78-B605-19C36563402C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
23
buildspec.yml
Normal file
23
buildspec.yml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
version: 0.2
|
||||||
|
|
||||||
|
phases:
|
||||||
|
pre_build:
|
||||||
|
commands:
|
||||||
|
- echo Logging in to Amazon ECR...
|
||||||
|
- aws --version
|
||||||
|
- aws ecr get-login-password --region eu-central-1 | docker login --username AWS --password-stdin 525595969507.dkr.ecr.eu-central-1.amazonaws.com
|
||||||
|
- REPOSITORY_URI=525595969507.dkr.ecr.eu-central-1.amazonaws.com/qwan/vendingmachine_cs
|
||||||
|
- COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
|
||||||
|
- IMAGE_TAG=${COMMIT_HASH:=latest}
|
||||||
|
build:
|
||||||
|
commands:
|
||||||
|
- echo Build started on `date`
|
||||||
|
- echo Building the Docker image...
|
||||||
|
- docker build -t $REPOSITORY_URI:latest .
|
||||||
|
- docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
|
||||||
|
post_build:
|
||||||
|
commands:
|
||||||
|
- echo Build completed on `date`
|
||||||
|
- echo Pushing the Docker images...
|
||||||
|
- docker push $REPOSITORY_URI:latest
|
||||||
|
- docker push $REPOSITORY_URI:$IMAGE_TAG
|
||||||
BIN
external_libs/NMock2.dll
Normal file
BIN
external_libs/NMock2.dll
Normal file
Binary file not shown.
BIN
external_libs/TestDriven.NET-3.0.2749_Personal.zip
Normal file
BIN
external_libs/TestDriven.NET-3.0.2749_Personal.zip
Normal file
Binary file not shown.
BIN
external_libs/nunit.framework.dll
Normal file
BIN
external_libs/nunit.framework.dll
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user