@grimoire-cc/cli 0.13.3 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/commands/update.d.ts.map +1 -1
  2. package/dist/commands/update.js +14 -0
  3. package/dist/commands/update.js.map +1 -1
  4. package/dist/enforce.d.ts +3 -1
  5. package/dist/enforce.d.ts.map +1 -1
  6. package/dist/enforce.js +18 -6
  7. package/dist/enforce.js.map +1 -1
  8. package/dist/setup.d.ts.map +1 -1
  9. package/dist/setup.js +47 -0
  10. package/dist/setup.js.map +1 -1
  11. package/dist/summary.d.ts.map +1 -1
  12. package/dist/summary.js +9 -0
  13. package/dist/summary.js.map +1 -1
  14. package/package.json +1 -1
  15. package/packs/dev-pack/agents/grimoire.tdd-specialist.md +194 -27
  16. package/packs/dev-pack/grimoire.json +0 -38
  17. package/packs/dev-pack/skills/grimoire.conventional-commit/SKILL.md +69 -65
  18. package/packs/dotnet-pack/agents/grimoire.csharp-coder.md +110 -113
  19. package/packs/dotnet-pack/grimoire.json +23 -5
  20. package/packs/dotnet-pack/skills/grimoire.unit-testing-dotnet/SKILL.md +252 -0
  21. package/packs/{dev-pack/skills/grimoire.tdd-specialist → dotnet-pack/skills/grimoire.unit-testing-dotnet}/reference/anti-patterns.md +78 -0
  22. package/packs/dotnet-pack/skills/grimoire.unit-testing-dotnet/reference/tdd-workflow-patterns.md +259 -0
  23. package/packs/go-pack/grimoire.json +19 -0
  24. package/packs/go-pack/skills/grimoire.unit-testing-go/SKILL.md +256 -0
  25. package/packs/go-pack/skills/grimoire.unit-testing-go/reference/anti-patterns.md +244 -0
  26. package/packs/go-pack/skills/grimoire.unit-testing-go/reference/tdd-workflow-patterns.md +259 -0
  27. package/packs/python-pack/grimoire.json +19 -0
  28. package/packs/python-pack/skills/grimoire.unit-testing-python/SKILL.md +239 -0
  29. package/packs/python-pack/skills/grimoire.unit-testing-python/reference/anti-patterns.md +244 -0
  30. package/packs/python-pack/skills/grimoire.unit-testing-python/reference/tdd-workflow-patterns.md +259 -0
  31. package/packs/rust-pack/grimoire.json +29 -0
  32. package/packs/rust-pack/skills/grimoire.unit-testing-rust/SKILL.md +243 -0
  33. package/packs/rust-pack/skills/grimoire.unit-testing-rust/reference/anti-patterns.md +244 -0
  34. package/packs/rust-pack/skills/grimoire.unit-testing-rust/reference/tdd-workflow-patterns.md +259 -0
  35. package/packs/ts-pack/agents/grimoire.typescript-coder.md +36 -1
  36. package/packs/ts-pack/grimoire.json +27 -1
  37. package/packs/ts-pack/skills/grimoire.unit-testing-typescript/SKILL.md +255 -0
  38. package/packs/ts-pack/skills/grimoire.unit-testing-typescript/reference/anti-patterns.md +244 -0
  39. package/packs/ts-pack/skills/grimoire.unit-testing-typescript/reference/tdd-workflow-patterns.md +259 -0
  40. package/packs/dev-pack/skills/grimoire.tdd-specialist/SKILL.md +0 -248
  41. package/packs/dev-pack/skills/grimoire.tdd-specialist/reference/language-frameworks.md +0 -388
  42. package/packs/dev-pack/skills/grimoire.tdd-specialist/reference/tdd-workflow-patterns.md +0 -135
  43. package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/SKILL.md +0 -293
  44. package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/reference/anti-patterns.md +0 -329
  45. package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/reference/framework-guidelines.md +0 -361
  46. package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/reference/parameterized-testing.md +0 -378
  47. package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/reference/test-organization.md +0 -476
  48. package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/reference/test-performance.md +0 -576
  49. package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/templates/tunit-template.md +0 -438
  50. package/packs/dotnet-pack/skills/grimoire.dotnet-unit-testing/templates/xunit-template.md +0 -303
@@ -1,248 +0,0 @@
1
- ---
2
- name: grimoire.tdd-specialist
3
- description: "Language-agnostic TDD and unit testing specialist. Use when writing unit tests, adding test coverage, doing test-driven development, or setting up test infrastructure in ANY language. Auto-detects project language and test framework. Supports pytest, jest, vitest, mocha, junit, go test, cargo test, xunit, and more. Triggers: tdd, test-driven, unit test, write tests, test coverage, red-green-refactor."
4
- version: 1.0.0
5
- ---
6
-
7
- # TDD Specialist
8
-
9
- Language-agnostic test-driven development and unit testing guidance. Works with any language — detects the project's test stack automatically and applies universal TDD principles.
10
-
11
- ## Context
12
-
13
- Test-driven development produces cleaner designs, fewer bugs, and enables confident refactoring. AI-assisted TDD works best with a structured workflow: humans define goals and approve test plans, AI executes the implementation. This skill provides the knowledge base for that workflow.
14
-
15
- ## Language & Framework Detection
16
-
17
- ### Step 1: Detect Language
18
-
19
- Check for project manifest files to determine the primary language:
20
-
21
- | File | Language |
22
- |------|----------|
23
- | `package.json` | JavaScript / TypeScript |
24
- | `tsconfig.json` | TypeScript |
25
- | `pyproject.toml`, `setup.py`, `setup.cfg` | Python |
26
- | `go.mod` | Go |
27
- | `Cargo.toml` | Rust |
28
- | `*.csproj`, `*.sln` | C# / .NET |
29
- | `pom.xml`, `build.gradle`, `build.gradle.kts` | Java / Kotlin |
30
- | `Gemfile` | Ruby |
31
- | `mix.exs` | Elixir |
32
- | `Package.swift` | Swift |
33
-
34
- ### Step 2: Detect Test Framework
35
-
36
- **Always check existing test files first** — match whatever the project already uses.
37
-
38
- If no existing tests, infer from config:
39
-
40
- - **JavaScript/TypeScript**: Check `package.json` devDependencies for `vitest`, `jest`, `mocha`. Default: Vitest for Vite projects, Jest otherwise.
41
- - **Python**: Check for `pytest` in dependencies or `[tool.pytest]` in `pyproject.toml`. Default: pytest.
42
- - **Go**: Built-in `testing` package. Check for `testify` in `go.mod`.
43
- - **Rust**: Built-in `#[test]`. Check for `mockall` in `Cargo.toml`.
44
- - **C#/.NET**: Check `.csproj` for xUnit/NUnit/MSTest references. **If `grimoire.dotnet-unit-testing` skill is available, defer to it.**
45
- - **Java/Kotlin**: Check for JUnit 5 (`junit-jupiter`), Mockito in build files. Default: JUnit 5 + Mockito.
46
- - **Ruby**: Check for `rspec` or `minitest` in Gemfile. Default: RSpec.
47
-
48
- ### Step 3: Detect Conventions
49
-
50
- Read 2-3 existing test files to learn:
51
- - File naming convention (e.g., `test_*.py`, `*.test.ts`, `*_test.go`)
52
- - Directory structure (e.g., `tests/`, `__tests__/`, co-located)
53
- - Assertion style and helper patterns
54
- - Mocking approach
55
-
56
- ## Workflow
57
-
58
- ### Step 1: Analyze
59
-
60
- - Read the source code under test
61
- - Detect language and test framework (steps above)
62
- - Identify dependencies that need mocking/stubbing
63
- - Check for existing test patterns in the project
64
- - Understand the expected behavior and edge cases
65
-
66
- ### Step 2: Plan (REQUIRES USER APPROVAL)
67
-
68
- Present test cases as **method/function names only**, grouped by category. Do NOT include test bodies.
69
-
70
- **Format:**
71
-
72
- ```plain
73
- ## Test Plan for [Module/Class.Method]
74
-
75
- **Language:** [detected] | **Framework:** [detected] | **File:** [proposed test file path]
76
-
77
- ### Success Scenarios
78
- - test_process_order_with_valid_input_returns_success
79
- - test_process_order_with_discount_applies_correctly
80
-
81
- ### Validation Failures
82
- - test_process_order_with_null_input_raises_value_error
83
- - test_process_order_with_empty_items_raises_validation_error
84
-
85
- ### Error Handling
86
- - test_process_order_when_repository_fails_raises_service_error
87
-
88
- ### Edge Cases
89
- - test_process_order_with_maximum_items_succeeds
90
-
91
- Do you approve this test plan? I will proceed only after your confirmation.
92
- ```
93
-
94
- **STOP and WAIT for user approval before proceeding.**
95
-
96
- ### Step 3: Write (ONLY after approval)
97
-
98
- Implement tests following:
99
- - Detected framework conventions
100
- - AAA (Arrange-Act-Assert) or Given-When-Then pattern
101
- - Language-idiomatic naming and style
102
- - Proper mocking/stubbing at boundaries
103
-
104
- ### Step 4: Explain
105
-
106
- - Present the complete test file
107
- - Explain what each test validates
108
- - Highlight assumptions made
109
- - Suggest additional scenarios if relevant
110
-
111
- ## Universal Testing Principles
112
-
113
- ### Arrange-Act-Assert (AAA)
114
-
115
- Structure every test with clearly separated phases. Use comments for clarity:
116
-
117
- ```python
118
- # Python / pytest
119
- def test_calculate_total_with_discount_applies_percentage():
120
- # Arrange
121
- cart = Cart(items=[Item(price=100), Item(price=50)])
122
- discount = PercentageDiscount(10)
123
-
124
- # Act
125
- total = cart.calculate_total(discount)
126
-
127
- # Assert
128
- assert total == 135.0
129
- ```
130
-
131
- ```typescript
132
- // TypeScript / Vitest
133
- test('calculateTotal with discount applies percentage', () => {
134
- // Arrange
135
- const cart = new Cart([{ price: 100 }, { price: 50 }]);
136
- const discount = new PercentageDiscount(10);
137
-
138
- // Act
139
- const total = cart.calculateTotal(discount);
140
-
141
- // Assert
142
- expect(total).toBe(135.0);
143
- });
144
- ```
145
-
146
- ```go
147
- // Go / testing
148
- func TestCalculateTotal_WithDiscount_AppliesPercentage(t *testing.T) {
149
- // Arrange
150
- cart := NewCart([]Item{{Price: 100}, {Price: 50}})
151
- discount := NewPercentageDiscount(10)
152
-
153
- // Act
154
- total := cart.CalculateTotal(discount)
155
-
156
- // Assert
157
- assert.Equal(t, 135.0, total)
158
- }
159
- ```
160
-
161
- ### Test Naming
162
-
163
- Use the language-idiomatic convention:
164
-
165
- | Language | Convention | Example |
166
- |----------|-----------|---------|
167
- | Python | `test_method_scenario_expected` | `test_get_user_with_invalid_id_raises_not_found` |
168
- | JS/TS | descriptive string | `'getUser with invalid id throws NotFound'` |
169
- | Go | `TestMethod_Scenario_Expected` | `TestGetUser_WithInvalidId_ReturnsNotFound` |
170
- | Rust | `test_method_scenario_expected` | `test_get_user_with_invalid_id_returns_not_found` |
171
- | Java/C# | `MethodName_Scenario_ExpectedBehavior` | `GetUser_WithInvalidId_ThrowsNotFoundException` |
172
- | Ruby | descriptive string (RSpec) | `'raises NotFound for invalid id'` |
173
-
174
- ### Test Isolation
175
-
176
- - Each test must be independent — no shared mutable state
177
- - Use setup/teardown (beforeEach, setUp, constructor) for fresh fixtures
178
- - Tests must pass in any order and in parallel
179
- - Never depend on external services, file system, or network in unit tests
180
-
181
- ### Mocking Boundaries
182
-
183
- - Mock/stub at system boundaries only (databases, APIs, file system, clock)
184
- - Do NOT mock the class under test
185
- - Do NOT mock value objects or simple data structures
186
- - Prefer fakes/stubs over mocks when possible — verify state, not interactions
187
- - Use dependency injection to make code testable
188
-
189
- ### One Assertion Focus
190
-
191
- Each test should verify ONE logical concept. Multiple `assert` calls are fine if they verify aspects of the same behavior:
192
-
193
- ```python
194
- # Good — one concept (successful creation), multiple assertions
195
- def test_create_user_with_valid_data_returns_user():
196
- user = create_user(name="Alice", email="alice@example.com")
197
- assert user.name == "Alice"
198
- assert user.email == "alice@example.com"
199
- assert user.id is not None
200
-
201
- # Bad — testing two unrelated behaviors in one test
202
- def test_user_creation_and_deletion():
203
- user = create_user(name="Alice")
204
- assert user.id is not None
205
- delete_user(user.id)
206
- assert get_user(user.id) is None # This is a separate test
207
- ```
208
-
209
- ### Test Behavior, Not Implementation
210
-
211
- Tests should verify WHAT the code does, not HOW it does it:
212
-
213
- ```typescript
214
- // Good — tests the result
215
- expect(sort([3, 1, 2])).toEqual([1, 2, 3]);
216
-
217
- // Bad — tests that a specific algorithm was used
218
- expect(quickSort).toHaveBeenCalledWith([3, 1, 2]);
219
- ```
220
-
221
- ## Constraints
222
-
223
- ### ALWAYS
224
-
225
- - ALWAYS detect language and framework before writing tests
226
- - ALWAYS check for existing test files and match their conventions
227
- - ALWAYS present test plan as method names ONLY before writing
228
- - ALWAYS ask for explicit approval: "Do you approve this test plan?"
229
- - ALWAYS use AAA pattern with section comments
230
- - ALWAYS use language-idiomatic naming conventions
231
- - ALWAYS isolate tests — no shared mutable state between tests
232
-
233
- ### NEVER
234
-
235
- - NEVER write test implementations until user explicitly approves the plan
236
- - NEVER create production code — only test code
237
- - NEVER mock the class/module under test
238
- - NEVER write tests that depend on execution order
239
- - NEVER use real external services (databases, APIs) in unit tests
240
- - NEVER ignore existing project test conventions
241
-
242
- ## Reference Materials
243
-
244
- For detailed guidance on specific topics:
245
-
246
- - **[Language Frameworks](reference/language-frameworks.md)** — Framework-specific patterns, assertions, and setup for each language
247
- - **[Anti-Patterns](reference/anti-patterns.md)** — Common testing mistakes and how to fix them
248
- - **[TDD Workflow Patterns](reference/tdd-workflow-patterns.md)** — Red-Green-Refactor, Transformation Priority Premise, when to use TDD
@@ -1,388 +0,0 @@
1
- # Language & Framework Reference
2
-
3
- Quick-reference for each language's test ecosystem. Use this to apply framework-specific patterns after detecting the project's language.
4
-
5
- ## Table of Contents
6
-
7
- - [JavaScript / TypeScript](#javascript--typescript)
8
- - [Python](#python)
9
- - [Go](#go)
10
- - [Rust](#rust)
11
- - [Java / Kotlin](#java--kotlin)
12
- - [C# / .NET](#c--net)
13
- - [Ruby](#ruby)
14
-
15
- ## JavaScript / TypeScript
16
-
17
- ### Frameworks
18
-
19
- | Framework | When to Use | Key Feature |
20
- |-----------|-------------|-------------|
21
- | **Vitest** | Vite projects, new TS projects | Fast, ESM-native, Jest-compatible API |
22
- | **Jest** | React (CRA), existing Jest projects | Mature ecosystem, wide adoption |
23
- | **Mocha + Chai** | Legacy projects, custom setups | Flexible, pluggable |
24
- | **Node test runner** | Node.js 20+, minimal deps | Built-in, zero dependencies |
25
-
26
- ### Vitest / Jest Patterns
27
-
28
- ```typescript
29
- import { describe, test, expect, vi, beforeEach } from 'vitest';
30
-
31
- describe('OrderService', () => {
32
- let mockRepo: MockedObject<OrderRepository>;
33
- let service: OrderService;
34
-
35
- beforeEach(() => {
36
- mockRepo = { save: vi.fn(), findById: vi.fn() };
37
- service = new OrderService(mockRepo);
38
- });
39
-
40
- test('processOrder with valid order saves and returns id', async () => {
41
- // Arrange
42
- const order = createValidOrder();
43
- mockRepo.save.mockResolvedValue({ id: '123' });
44
-
45
- // Act
46
- const result = await service.processOrder(order);
47
-
48
- // Assert
49
- expect(result.id).toBe('123');
50
- expect(mockRepo.save).toHaveBeenCalledWith(order);
51
- });
52
-
53
- test('processOrder with invalid order throws ValidationError', async () => {
54
- // Arrange
55
- const order = createInvalidOrder();
56
-
57
- // Act & Assert
58
- await expect(service.processOrder(order)).rejects.toThrow(ValidationError);
59
- });
60
- });
61
- ```
62
-
63
- ### File Conventions
64
-
65
- - `*.test.ts` / `*.spec.ts` (co-located or in `__tests__/`)
66
- - `vitest.config.ts` or `jest.config.ts` for configuration
67
-
68
- ## Python
69
-
70
- ### Frameworks
71
-
72
- | Framework | When to Use | Key Feature |
73
- |-----------|-------------|-------------|
74
- | **pytest** | Default choice, most projects | Fixtures, parametrize, plugins |
75
- | **unittest** | stdlib only, legacy projects | Built-in, class-based |
76
-
77
- ### pytest Patterns
78
-
79
- ```python
80
- import pytest
81
- from unittest.mock import Mock, AsyncMock, patch
82
-
83
- @pytest.fixture
84
- def mock_repo():
85
- repo = Mock(spec=OrderRepository)
86
- repo.save = AsyncMock(return_value=Order(id="123"))
87
- return repo
88
-
89
- @pytest.fixture
90
- def service(mock_repo):
91
- return OrderService(repository=mock_repo)
92
-
93
- async def test_process_order_with_valid_order_returns_id(service, mock_repo):
94
- # Arrange
95
- order = create_valid_order()
96
-
97
- # Act
98
- result = await service.process_order(order)
99
-
100
- # Assert
101
- assert result.id == "123"
102
- mock_repo.save.assert_called_once_with(order)
103
-
104
- async def test_process_order_with_invalid_order_raises_validation_error(service):
105
- # Arrange
106
- order = create_invalid_order()
107
-
108
- # Act & Assert
109
- with pytest.raises(ValidationError, match="items cannot be empty"):
110
- await service.process_order(order)
111
-
112
- @pytest.mark.parametrize("discount,expected", [
113
- (0, 100.0),
114
- (10, 90.0),
115
- (50, 50.0),
116
- ])
117
- def test_apply_discount_calculates_correctly(discount, expected):
118
- assert apply_discount(100.0, discount) == expected
119
- ```
120
-
121
- ### File Conventions
122
-
123
- - `test_*.py` or `*_test.py` in `tests/` directory
124
- - `conftest.py` for shared fixtures
125
- - `pytest.ini` or `[tool.pytest.ini_options]` in `pyproject.toml`
126
-
127
- ## Go
128
-
129
- ### Frameworks
130
-
131
- | Framework | When to Use | Key Feature |
132
- |-----------|-------------|-------------|
133
- | **testing** (stdlib) | Always available | Built-in, no dependencies |
134
- | **testify** | Assertions + mocking | `assert`, `require`, `mock` packages |
135
- | **gomock** | Interface mocking | Code generation, strict expectations |
136
-
137
- ### Go Patterns
138
-
139
- ```go
140
- func TestProcessOrder_WithValidOrder_ReturnsID(t *testing.T) {
141
- // Arrange
142
- repo := new(MockOrderRepository)
143
- repo.On("Save", mock.Anything).Return(&Order{ID: "123"}, nil)
144
- service := NewOrderService(repo)
145
-
146
- // Act
147
- result, err := service.ProcessOrder(context.Background(), validOrder)
148
-
149
- // Assert
150
- require.NoError(t, err)
151
- assert.Equal(t, "123", result.ID)
152
- repo.AssertExpectations(t)
153
- }
154
-
155
- func TestProcessOrder_WithInvalidOrder_ReturnsError(t *testing.T) {
156
- // Arrange
157
- service := NewOrderService(nil)
158
-
159
- // Act
160
- _, err := service.ProcessOrder(context.Background(), invalidOrder)
161
-
162
- // Assert
163
- assert.ErrorIs(t, err, ErrValidation)
164
- }
165
-
166
- // Table-driven tests (Go idiom)
167
- func TestApplyDiscount(t *testing.T) {
168
- tests := []struct {
169
- name string
170
- price float64
171
- discount int
172
- expected float64
173
- }{
174
- {"no discount", 100.0, 0, 100.0},
175
- {"10 percent", 100.0, 10, 90.0},
176
- {"50 percent", 100.0, 50, 50.0},
177
- }
178
- for _, tt := range tests {
179
- t.Run(tt.name, func(t *testing.T) {
180
- result := ApplyDiscount(tt.price, tt.discount)
181
- assert.Equal(t, tt.expected, result)
182
- })
183
- }
184
- }
185
- ```
186
-
187
- ### File Conventions
188
-
189
- - `*_test.go` in the same package (or `_test` package for black-box)
190
- - `go test ./...` to run all tests
191
- - `testdata/` for test fixtures
192
-
193
- ## Rust
194
-
195
- ### Patterns
196
-
197
- ```rust
198
- #[cfg(test)]
199
- mod tests {
200
- use super::*;
201
- use mockall::predicate::*;
202
-
203
- #[test]
204
- fn process_order_with_valid_order_returns_id() {
205
- // Arrange
206
- let mut mock_repo = MockOrderRepository::new();
207
- mock_repo.expect_save()
208
- .returning(|_| Ok(Order { id: "123".into() }));
209
- let service = OrderService::new(Box::new(mock_repo));
210
-
211
- // Act
212
- let result = service.process_order(&valid_order());
213
-
214
- // Assert
215
- assert_eq!(result.unwrap().id, "123");
216
- }
217
-
218
- #[test]
219
- fn process_order_with_invalid_order_returns_error() {
220
- // Arrange
221
- let service = OrderService::new(Box::new(MockOrderRepository::new()));
222
-
223
- // Act
224
- let result = service.process_order(&invalid_order());
225
-
226
- // Assert
227
- assert!(matches!(result, Err(ServiceError::Validation(_))));
228
- }
229
- }
230
- ```
231
-
232
- ### File Conventions
233
-
234
- - Tests in `#[cfg(test)] mod tests` at bottom of source file (unit tests)
235
- - Integration tests in `tests/` directory
236
- - `cargo test` to run all
237
-
238
- ## Java / Kotlin
239
-
240
- ### Frameworks
241
-
242
- | Framework | When to Use | Key Feature |
243
- |-----------|-------------|-------------|
244
- | **JUnit 5** | Default choice | Modern, extensible, parameterized |
245
- | **Mockito** | Mocking | Intuitive API, wide adoption |
246
- | **AssertJ** | Fluent assertions | Readable, discoverable API |
247
-
248
- ### JUnit 5 + Mockito Patterns
249
-
250
- ```java
251
- @ExtendWith(MockitoExtension.class)
252
- class OrderServiceTest {
253
- @Mock OrderRepository repository;
254
- @InjectMocks OrderService service;
255
-
256
- @Test
257
- void processOrder_withValidOrder_returnsId() {
258
- // Arrange
259
- var order = createValidOrder();
260
- when(repository.save(any())).thenReturn(new Order("123"));
261
-
262
- // Act
263
- var result = service.processOrder(order);
264
-
265
- // Assert
266
- assertThat(result.getId()).isEqualTo("123");
267
- verify(repository).save(order);
268
- }
269
-
270
- @Test
271
- void processOrder_withInvalidOrder_throwsValidationException() {
272
- // Arrange
273
- var order = createInvalidOrder();
274
-
275
- // Act & Assert
276
- assertThatThrownBy(() -> service.processOrder(order))
277
- .isInstanceOf(ValidationException.class)
278
- .hasMessageContaining("items cannot be empty");
279
- }
280
-
281
- @ParameterizedTest
282
- @CsvSource({"0, 100.0", "10, 90.0", "50, 50.0"})
283
- void applyDiscount_calculatesCorrectly(int discount, double expected) {
284
- assertThat(applyDiscount(100.0, discount)).isEqualTo(expected);
285
- }
286
- }
287
- ```
288
-
289
- ### File Conventions
290
-
291
- - `src/test/java/` mirroring `src/main/java/` structure
292
- - `*Test.java` suffix
293
- - `mvn test` or `gradle test`
294
-
295
- ## C# / .NET
296
-
297
- **If the `grimoire.dotnet-unit-testing` skill is available, defer to it for full C#/.NET guidance.** It provides comprehensive xUnit, TUnit, Moq, and NSubstitute patterns.
298
-
299
- ### Quick Reference (when dotnet skill is unavailable)
300
-
301
- | Framework | When to Use |
302
- |-----------|-------------|
303
- | **xUnit** | Default, most universal |
304
- | **NUnit** | If project already uses it |
305
- | **MSTest** | Microsoft-first shops |
306
-
307
- ```csharp
308
- public class OrderServiceTests
309
- {
310
- private readonly Mock<IOrderRepository> _mockRepo;
311
- private readonly OrderService _sut;
312
-
313
- public OrderServiceTests()
314
- {
315
- _mockRepo = new Mock<IOrderRepository>();
316
- _sut = new OrderService(_mockRepo.Object);
317
- }
318
-
319
- [Fact]
320
- public async Task ProcessOrder_WithValidOrder_ReturnsId()
321
- {
322
- // Arrange
323
- var order = CreateValidOrder();
324
- _mockRepo.Setup(r => r.SaveAsync(It.IsAny<Order>()))
325
- .ReturnsAsync(new Order { Id = "123" });
326
-
327
- // Act
328
- var result = await _sut.ProcessOrderAsync(order);
329
-
330
- // Assert
331
- Assert.Equal("123", result.Id);
332
- }
333
- }
334
- ```
335
-
336
- ### File Conventions
337
-
338
- - `Tests/` or `*.Tests/` project mirroring source structure
339
- - `*Tests.cs` suffix
340
- - `dotnet test` to run
341
-
342
- ## Ruby
343
-
344
- ### Frameworks
345
-
346
- | Framework | When to Use |
347
- |-----------|-------------|
348
- | **RSpec** | Default, most Ruby projects |
349
- | **Minitest** | stdlib, Rails default |
350
-
351
- ### RSpec Patterns
352
-
353
- ```ruby
354
- RSpec.describe OrderService do
355
- let(:repository) { instance_double(OrderRepository) }
356
- let(:service) { described_class.new(repository: repository) }
357
-
358
- describe '#process_order' do
359
- context 'with a valid order' do
360
- it 'returns the order id' do
361
- # Arrange
362
- order = build(:order, :valid)
363
- allow(repository).to receive(:save).and_return(Order.new(id: '123'))
364
-
365
- # Act
366
- result = service.process_order(order)
367
-
368
- # Assert
369
- expect(result.id).to eq('123')
370
- expect(repository).to have_received(:save).with(order)
371
- end
372
- end
373
-
374
- context 'with an invalid order' do
375
- it 'raises ValidationError' do
376
- order = build(:order, :invalid)
377
- expect { service.process_order(order) }.to raise_error(ValidationError)
378
- end
379
- end
380
- end
381
- end
382
- ```
383
-
384
- ### File Conventions
385
-
386
- - `spec/` directory mirroring `lib/` or `app/` structure
387
- - `*_spec.rb` suffix
388
- - `bundle exec rspec` to run