]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
docs(tests): enhance TEST_STYLE_GUIDE with async patterns and best practices
authorJérôme Benoit <jerome.benoit@sap.com>
Sat, 28 Feb 2026 13:26:32 +0000 (14:26 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Sat, 28 Feb 2026 13:26:32 +0000 (14:26 +0100)
- Add Testing Philosophy section with core principles
- Add Async Testing Patterns section (async/await, Promise rejection, timeout)
- Add Error & Exception Testing section with examples
- Soften AAA comments rule (required for complex, optional for simple tests)
- Add anti-pattern #6: Non-strict assertions guidance
- Align with Node.js test runner best practices 2024-2025

tests/TEST_STYLE_GUIDE.md

index 2fc517bf04cb69ba15d2144607f830cc5b3a6305..7450b2bd4f24ca6fd62a6dc67511a01e2135c316 100644 (file)
@@ -2,6 +2,16 @@
 
 This document establishes conventions for writing maintainable, consistent tests in the e-mobility charging stations simulator project.
 
+## Testing Philosophy
+
+Core principles guiding test implementation:
+
+- **Test behavior, not implementation**: Focus on what code does, not how it does it
+- **Isolation is mandatory**: Each test must run independently with fresh state
+- **Determinism required**: Tests must produce identical results on every run
+- **Coverage target**: Aim for 80%+ code coverage on new code
+- **Strict assertions**: Use strict equality (`toBe`, `toStrictEqual`) to prevent false positives
+
 ## Naming Conventions
 
 ### Test Case Naming (MANDATORY)
@@ -56,7 +66,12 @@ Follow the Arrange-Act-Assert pattern for clarity:
 2. **Act**: Execute the code under test
 3. **Assert**: Verify the expected outcome
 
-**Example:**
+### When to Use AAA Comments
+
+- **Required**: Tests with 3+ setup steps or complex assertions
+- **Optional**: Simple single-assertion tests where intent is obvious
+
+**Complex test (comments required):**
 
 ```typescript
 it('should calculate total power correctly', () => {
@@ -72,6 +87,126 @@ it('should calculate total power correctly', () => {
 })
 ```
 
+**Simple test (comments optional):**
+
+```typescript
+it('should return true for valid identifier', () => {
+  expect(isValidIdentifier('ABC123')).toBe(true)
+})
+```
+
+## Async Testing Patterns
+
+Most tests in this project are asynchronous. Follow these patterns:
+
+### Async/Await (Preferred)
+
+✅ **Good:**
+
+```typescript
+it('should start charging session successfully', async () => {
+  // Arrange
+  const station = await createChargingStation({ connectorsCount: 2 })
+  const connectorId = 1
+
+  // Act
+  const result = await station.startTransaction(connectorId, 'VALID_TAG')
+
+  // Assert
+  expect(result.status).toBe('Accepted')
+  expect(station.getConnectorStatus(connectorId)?.transactionStarted).toBe(true)
+})
+```
+
+### Promise Rejection Testing
+
+```typescript
+it('should reject invalid connector ID', async () => {
+  const station = await createChargingStation({ connectorsCount: 1 })
+
+  await expect(station.startTransaction(99, 'TAG')).rejects.toThrow('Invalid connector')
+})
+```
+
+### Timeout Handling
+
+```typescript
+it('should timeout when server does not respond', async () => {
+  mock.timers.enable({ apis: ['setTimeout'] })
+  const station = await createChargingStation()
+
+  const responsePromise = station.sendHeartbeat()
+  mock.timers.tick(30000) // Advance past timeout
+
+  await expect(responsePromise).rejects.toThrow('Timeout')
+  mock.timers.reset()
+})
+```
+
+❌ **Bad (Mixing callbacks and Promises):**
+
+```typescript
+// WRONG: Never mix callback and Promise patterns
+it('broken test', done => {
+  someAsyncOp().then(() => {
+    done() // Confusing - use async/await instead
+  })
+})
+```
+
+## Error & Exception Testing
+
+Error handling is critical. Test both expected errors and edge cases:
+
+### Testing Expected Errors
+
+```typescript
+it('should throw on invalid configuration', () => {
+  expect(() => new ChargingStation(null)).toThrow('Configuration required')
+})
+
+it('should reject unauthorized tag', async () => {
+  const station = await createChargingStation()
+
+  await expect(station.authorize('INVALID_TAG')).rejects.toThrow(OCPPError)
+})
+```
+
+### Testing Error Properties
+
+```typescript
+it('should include error code in OCPPError', async () => {
+  const station = await createChargingStation()
+
+  try {
+    await station.sendInvalidCommand()
+    expect.fail('Should have thrown')
+  } catch (error) {
+    expect(error).toBeInstanceOf(OCPPError)
+    expect((error as OCPPError).code).toBe('GenericError')
+  }
+})
+```
+
+### Testing Error Recovery
+
+```typescript
+it('should recover after transient error', async () => {
+  const station = await createChargingStation()
+  mock.method(station, 'sendMessage', () => {
+    throw new Error('Network error')
+  })
+
+  // First call fails
+  await expect(station.sendHeartbeat()).rejects.toThrow('Network')
+
+  // Restore and retry succeeds
+  mock.restoreAll()
+  const result = await station.sendHeartbeat()
+  expect(result).toBeDefined()
+})
+```
+
 ## Comments & JSDoc
 
 ### File Headers
@@ -384,6 +519,28 @@ expect(isAsyncFunction(() => {})).toBe(false)
 - Disabling rules to bypass type safety in test setup
 - Disabling rules because proper interfaces haven't been created
 
+### 6. Non-Strict Assertions
+
+❌ **Bad:**
+
+```typescript
+// Loose equality - can cause false positives
+expect(result).toEqual({ status: 'ok' }) // Ignores extra properties
+expect(count == '5').toBe(true) // Type coercion
+expect(value).toBeTruthy() // Too vague
+```
+
+✅ **Good:**
+
+```typescript
+// Strict equality - catches more bugs
+expect(result).toStrictEqual({ status: 'ok' }) // Exact match
+expect(count).toBe(5) // Type-safe
+expect(value).toBe(true) // Explicit
+```
+
+**Why:** Strict assertions catch type mismatches and unexpected properties. Use `toBe()` for primitives, `toStrictEqual()` for objects.
+
 ## Summary
 
 - **Name clearly**: Descriptive names for files, suites, and test cases