Backend Testing Skill
Description
Specialized agent for implementing meaningful tests in the backend (Node.js/TypeScript) following TDD principles and best practices.
Context
- •Project: kokkai-join backend (Node.js, TypeScript, Vitest, Hono, Supabase)
- •Testing framework: Vitest
- •Database: PostgreSQL with Supabase
- •Focus: Writing tests that verify actual behavior, not just function calls
When to Use This Skill
- •Implementing new backend features (API endpoints, services, utilities)
- •Writing tests for existing untested code
- •Refactoring tests that are meaningless or low-value
- •Adding integration tests for database operations
Testing Principles (MUST FOLLOW)
1. TDD Approach is MANDATORY
- •RED: Write a failing test first
- •GREEN: Write minimum code to pass
- •REFACTOR: Improve code quality
2. What Makes a Meaningful Test
✅ GOOD Tests
- •Test behavior and business logic, not implementation details
- •Use appropriate levels of mocking (mock boundaries, not internals)
- •Cover edge cases, error conditions, and boundary values
- •Provide clear failure messages
- •Are refactoring-resistant (don't break on internal changes)
❌ BAD Tests
- •Mock everything and only verify function calls were made
- •Test implementation details (e.g.,
expect(mockClient._mocks.from).toHaveBeenCalledWith(...)) - •Have vague assertions like
expect(result).toBeDefined() - •Are written just for coverage without verifying correctness
3. Test Types and Guidelines
Unit Tests
- •Purpose: Test pure functions, business logic, data transformations
- •Good example:
markdown-converter.test.ts,duplication-analyzer.test.ts - •What to test: Input/output transformations, edge cases, error handling
- •What NOT to do: Mock database clients and only test that functions were called
Integration Tests
- •Purpose: Test service layer logic, multi-component interactions
- •Good example:
multi-source-search.test.ts - •What to test: How components work together, orchestration logic
- •What to mock: External providers (using test implementations), not the service itself
Database/API Tests
- •Purpose: Test actual database operations and API endpoints
- •AVOID: Full Supabase client mocking (like
search-history-api.test.ts) - •BETTER: Use test database, Supabase local instance, or test transactions with rollback
- •What to test: Actual queries, RLS policies, data integrity
4. Specific Rules for This Project
When Testing Supabase Operations
❌ DON'T DO THIS (meaningless):
typescript
// Bad: Mocking the entire Supabase chain
mockClient._mocks.from.mockReturnValue({...})
mockClient._mocks.select.mockReturnValue({...})
expect(mockClient._mocks.from).toHaveBeenCalledWith("table_name")
✅ DO THIS INSTEAD:
typescript
// Good: Use Supabase local instance or test database
// Option 1: Supabase local development
const supabase = createClient(testUrl, testKey)
const result = await getSearchHistories(supabase)
expect(result).toHaveLength(expectedCount)
expect(result[0]).toMatchObject({ query: "expected query" })
// Option 2: Mock at HTTP level, not client level
nock('https://supabase-url.com')
.post('/rest/v1/search_histories')
.reply(200, mockData)
When Testing Services
✅ GOOD APPROACH:
- •Mock providers/external dependencies at interface boundaries
- •Test actual service logic
- •Verify business rules, error handling, data transformations
Example (from multi-source-search.test.ts):
typescript
// Good: Mock providers, test service orchestration
class MockProvider implements SearchProvider {
async search(query: ProviderQuery): Promise<DocumentResult[]> {
return this.results
}
}
const results = await service.searchAcross([provider1, provider2], query)
expect(results.length).toBe(expectedLength)
expect(results).toContainEqual(expectedDocument)
When Testing Utilities
✅ GOOD APPROACH:
- •Test pure functions thoroughly
- •Cover all edge cases and boundary conditions
- •No mocking needed for pure functions
Example (from markdown-converter.test.ts):
typescript
// Good: Test actual transformation logic
const markdown = convertDeepResearchToMarkdown(response)
expect(markdown).toContain("これはテストです[^1]")
expect(markdown).toContain("[^1]: [テスト資料](https://example.com/test)")
5. Error Cases to Avoid
❌ Testing Function Calls Instead of Behavior
typescript
// BAD
expect(mockClient._mocks.getUser).toHaveBeenCalled()
expect(mockClient._mocks.insert).toHaveBeenCalledWith(expect.objectContaining({...}))
// This only tests that functions were called, not that they work correctly
❌ Vague Assertions
typescript
// BAD expect(result).toBeDefined() expect(client).toBeDefined() // These don't verify correctness
❌ Testing Implementation Details
typescript
// BAD expect(service.internalHelperMethod()).toBe(...) // Internal methods can change without affecting behavior
Implementation Workflow
Step 1: Analyze Requirements
- •Understand the feature/bug to implement
- •Identify what behavior needs testing
- •Determine appropriate test type (unit, integration, e2e)
Step 2: Write Failing Test (RED)
- •Write a test that describes desired behavior
- •Run test to confirm it fails
- •Ensure failure message is clear
Step 3: Implement Minimum Code (GREEN)
- •Write simplest code to make test pass
- •Don't over-engineer
- •Run test to confirm it passes
Step 4: Refactor (if needed)
- •Improve code quality
- •Ensure all tests still pass
- •Don't change behavior
Step 5: Add Edge Cases
- •Test error conditions
- •Test boundary values
- •Test null/undefined/empty cases
Step 6: Verify Test Quality
- •Ask: "If this test fails, do I know what's wrong?"
- •Ask: "Can I refactor code without breaking this test?"
- •Ask: "Does this test verify actual behavior?"
Common Patterns
Testing API Endpoints (Hono)
typescript
describe("POST /api/v1/endpoint", () => {
it("should return 200 with valid data", async () => {
const app = createApp() // Real app instance
const res = await app.request("/api/v1/endpoint", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ data: "test" })
})
expect(res.status).toBe(200)
const json = await res.json()
expect(json).toMatchObject({ success: true })
})
it("should return 400 with invalid data", async () => {
// Test error case
})
})
Testing Services with Dependencies
typescript
describe("MyService", () => {
it("should orchestrate multiple providers", async () => {
const mockProvider1 = new MockProvider("p1", [doc1, doc2])
const mockProvider2 = new MockProvider("p2", [doc3])
const service = new MyService()
const results = await service.process([mockProvider1, mockProvider2])
// Test actual orchestration logic
expect(results).toHaveLength(3)
expect(results.map(r => r.id).sort()).toEqual(["doc1", "doc2", "doc3"])
})
})
Testing Pure Functions
typescript
describe("convertData", () => {
it("should convert valid input", () => {
const input = { ... }
const result = convertData(input)
expect(result).toEqual(expectedOutput)
})
it("should handle empty input", () => {
const result = convertData({})
expect(result).toEqual(defaultOutput)
})
it("should throw on invalid input", () => {
expect(() => convertData(null)).toThrow("Invalid input")
})
})
Output Format
When implementing tests, provide:
- •Test file path: Where the test should be created
- •Test description: What behavior is being tested
- •Test code: Following TDD principles
- •Run instructions: How to run the test
- •Explanation: Why this test is meaningful
Important Reminders
- •NEVER mock Supabase client internals and only test function calls
- •ALWAYS test actual behavior, not implementation details
- •PREFER integration tests over unit tests for database operations
- •USE test databases or Supabase local instance for DB tests
- •WRITE failing test first (RED), then implement (GREEN), then refactor
- •VERIFY tests are meaningful by asking: "What does this failure tell me?"
Examples of Meaningful Tests to Reference
- •✅
backend/utils/markdown-converter.test.ts- Pure function testing - •✅
backend/services/multi-source-search.test.ts- Service integration testing - •✅
backend/utils/duplication-analyzer.test.ts- Complex logic testing - •⚠️
backend/lib/search-history-api.test.ts- AVOID this pattern (too much mocking)