Files
lobehub/docs/development/tests/integration-testing.mdx
René Wang 3dfc86fd0f feat: Update user guide & changelog (#11518)
* feat: Redesign doc

* chore: uopdate site

* chore: uopdate site

* chore: uopdate site

* chore: uopdate site

* chore: uopdate site

* feat: Uopdate content

* chore: New doc

* chore: Update content

* chore: Update content

* chore: add images

* chore: add images

* chore: add images

* chore: add images

* feat: Add more images

* feat: Add more images

* fix: Cannot reach end

* chore: Update content

* chore: Update content

* chore: Update content

* chore: Update content

* chore: Update content

* Revise README content and structure

Updated README to reflect changes in project description and removed outdated notes.

* Revise 'Getting Started' and TOC in README

Updated the 'Getting Started' section and modified the table of contents.

* chore: Update content

* Revise README structure and content

Updated the Getting Started section and removed the Table of Contents. Adjusted the Local Development instructions.

* Remove custom themes section from README

Removed section about custom themes from README.

* Update README.md

* Refine introduction and highlight cloud version

Updated wording for clarity and added recommendation for cloud version.

* chore: Update content

* chore: Update content

* chore: Update content

* chore: Update content

* chore: Update content

* chore: Update content

* chore: Update content

* fix: add missing translation

* 🔀 chore: Move README changes to feat/readme branch

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add missing translation

* chore: update cdn

* docs: add migration guide from v1.x local database to v2.x and update help sections

Signed-off-by: Innei <tukon479@gmail.com>

* fix: add missing translation

* fix: add missing images

* fix: add missing changelogs

* fix: add missing changelogs

* fix: add missing changelogs

* fix: add missing changelogs

* fix: add missing changelogs

* style: update cdn

---------

Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: canisminor1990 <i@canisminor.cc>
Co-authored-by: Innei <tukon479@gmail.com>
2026-01-26 15:28:33 +08:00

411 lines
11 KiB
Plaintext

---
title: Integration Testing Guide
description: >-
Learn how to effectively conduct integration testing to verify module
interactions and data integrity.
tags:
- Integration Testing
- Software Testing
- Test Automation
---
# Integration Testing Guide
## Overview
Integration testing verifies the correctness of multiple modules working together, ensuring that the complete call chain (Router → Service → Model → Database) functions as expected.
## Why Do We Need Integration Tests?
Even with high unit test coverage (80%+), integration issues can still occur:
### Common Issue Example
```typescript
// ❌ Issue: Parameter lost in the call chain
// Router layer
const messageId = await messageModel.create({
content: 'test',
sessionId: 'xxx',
topicId: 'yyy', // ← topicId is passed in
});
// Model layer (assume there's a bug)
async create(data) {
return this.db.insert(messages).values({
content: data.content,
sessionId: data.sessionId,
// ❌ Forgot to pass topicId
});
}
// Result: Unit test passes (because Model is mocked), but topicId is lost in actual execution
```
### Issues Caught by Integration Tests
1. **Missing parameter propagation**: containerId, threadId, topicId, etc., lost in the call chain
2. **Database constraints**: foreign keys, cascading deletes, etc., cannot be verified with mocks
3. **Transaction integrity**: atomicity of cross-table operations
4. **Permission checks**: cross-user access control
5. **Real-world scenarios**: simulate complete user workflows
## Running Integration Tests
```bash
# Run all integration tests
pnpm test:integration
# Run a specific test file
pnpm vitest tests/integration/routers/message.integration.test.ts
# Watch mode
pnpm vitest tests/integration --watch
# Generate coverage report
pnpm test:integration --coverage
```
## Directory Structure
```
tests/integration/
├── README.md # Integration test documentation
├── setup.ts # Common setup and utility functions
└── routers/ # Router layer integration tests
├── message.integration.test.ts # Message Router tests
├── session.integration.test.ts # Session Router tests
├── topic.integration.test.ts # Topic Router tests
└── chat-flow.integration.test.ts # Full chat flow tests
```
## Writing Integration Tests
### Basic Template
```typescript
// @vitest-environment node
import { eq } from 'drizzle-orm';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { getTestDB } from '@/database/models/__tests__/_util';
import { messages, sessions, users } from '@/database/schemas';
import { LobeHubDatabase } from '@/database/type';
import { messageRouter } from '@/server/routers/lambda/message';
import { cleanupTestUser, createTestContext, createTestUser } from '../setup';
describe('Your Feature Integration Tests', () => {
let serverDB: LobeHubDatabase;
let userId: string;
beforeEach(async () => {
// 1. Get test database
serverDB = await getTestDB();
// 2. Create test user
userId = await createTestUser(serverDB);
// 3. Prepare other test data
// ...
});
afterEach(async () => {
// Clean up test data
await cleanupTestUser(serverDB, userId);
});
it('should do something', async () => {
// 1. Create tRPC caller
const caller = messageRouter.createCaller(createTestContext(userId));
// 2. Perform operation
const result = await caller.someMethod({
/* params */
});
// 3. Assert result
expect(result).toBeDefined();
// 4. 🔥 Key: Verify from database
const [dbRecord] = await serverDB.select().from(messages).where(eq(messages.id, result));
expect(dbRecord).toMatchObject({
// Verify all critical fields
});
});
});
```
### Best Practices
#### 1. Test the Full Call Chain
```typescript
it('should create message with correct associations', async () => {
const caller = messageRouter.createCaller(createTestContext(userId));
// Perform operation
const messageId = await caller.createMessage({
content: 'Test',
sessionId: testSessionId,
topicId: testTopicId,
});
// ✅ Verify from database, not just return value
const [message] = await serverDB.select().from(messages).where(eq(messages.id, messageId));
expect(message.sessionId).toBe(testSessionId);
expect(message.topicId).toBe(testTopicId);
expect(message.userId).toBe(userId);
});
```
#### 2. Test Cascading Operations
```typescript
it('should cascade delete messages when session is deleted', async () => {
const sessionCaller = sessionRouter.createCaller(createTestContext(userId));
const messageCaller = messageRouter.createCaller(createTestContext(userId));
// Create session and messages
const sessionId = await sessionCaller.createSession({
/* ... */
});
await messageCaller.createMessage({ sessionId /* ... */ });
// Delete session
await sessionCaller.removeSession({ id: sessionId });
// ✅ Verify related messages are also deleted
const remainingMessages = await serverDB
.select()
.from(messages)
.where(eq(messages.sessionId, sessionId));
expect(remainingMessages).toHaveLength(0);
});
```
#### 3. Test Cross-Router Collaboration
```typescript
it('should handle complete chat flow', async () => {
const sessionCaller = sessionRouter.createCaller(createTestContext(userId));
const topicCaller = topicRouter.createCaller(createTestContext(userId));
const messageCaller = messageRouter.createCaller(createTestContext(userId));
// 1. Create session
const sessionId = await sessionCaller.createSession({
/* ... */
});
// 2. Create topic
const topicId = await topicCaller.createTopic({ sessionId /* ... */ });
// 3. Create message
const messageId = await messageCaller.createMessage({
sessionId,
topicId,
/* ... */
});
// ✅ Verify full associations
const [message] = await serverDB.select().from(messages).where(eq(messages.id, messageId));
expect(message.sessionId).toBe(sessionId);
expect(message.topicId).toBe(topicId);
});
```
#### 4. Test Error Scenarios
```typescript
it('should prevent cross-user access', async () => {
// User A creates session
const sessionId = await sessionRouter.createCaller(createTestContext(userA)).createSession({
/* ... */
});
// User B tries to access
const callerB = messageRouter.createCaller(createTestContext(userB));
// ✅ Should throw error
await expect(
callerB.createMessage({
sessionId,
content: 'Unauthorized',
}),
).rejects.toThrow();
});
```
#### 5. Test Concurrency
```typescript
it('should handle concurrent operations', async () => {
const caller = messageRouter.createCaller(createTestContext(userId));
// Concurrently create multiple messages
const promises = Array.from({ length: 10 }, (_, i) =>
caller.createMessage({
content: `Message ${i}`,
sessionId: testSessionId,
}),
);
const messageIds = await Promise.all(promises);
// ✅ Verify all messages created successfully and are unique
expect(messageIds).toHaveLength(10);
expect(new Set(messageIds).size).toBe(10);
});
```
### Data Isolation
Each test case should be independent and not rely on others:
```typescript
beforeEach(async () => {
// Create new data for each test
userId = await createTestUser(serverDB);
testSessionId = await createTestSession(serverDB, userId);
});
afterEach(async () => {
// Clean up test data
await cleanupTestUser(serverDB, userId);
});
```
### Test Naming
Use clear names to describe the test's intent:
```typescript
// ✅ Good naming
it('should create message with correct sessionId and topicId');
it('should cascade delete messages when session is deleted');
it('should prevent cross-user access to messages');
// ❌ Poor naming
it('test message creation');
it('test delete');
```
## Differences from Unit Tests
| Dimension | Unit Test | Integration Test |
| ---------------- | --------------------------- | --------------------------------- |
| **Scope** | Single function/class | Multiple modules working together |
| **Dependencies** | Mocks external dependencies | Uses real dependencies |
| **Database** | Mocked | Real test database |
| **Speed** | Fast (ms level) | Slower (seconds) |
| **Quantity** | Many (60%) | Fewer (30%) |
| **Purpose** | Verify logic correctness | Verify integration correctness |
## Testing Pyramid
```
/\
/E2E\ ← 10% (Critical business flows)
/------\
/Integration\ ← 30% (API integration tests) ⭐ Focus of this guide
/------------\
/ Unit Tests \ ← 60% (Already 80%+ coverage)
/----------------\
```
## Coverage Goals
### Priority P0 (Must Cover)
- ✅ Cross-layer ID propagation (sessionId, topicId, containerId, threadId)
- ✅ Permission checks (users can only access their own resources)
- ✅ Cascading deletes (deleting a session also deletes related data)
- ✅ Foreign key constraints (cannot create associations to non-existent records)
### Priority P1 (Should Cover)
- Concurrency (multiple requests at the same time)
- Pagination (correct data slicing)
- Search functionality (keyword search)
- Batch operations (bulk create/delete)
### Priority P2 (Nice to Have)
- Analytics (counts, rankings)
- Complex queries (multi-condition filters)
- Performance testing (large data scenarios)
## Debugging Tips
### 1. Inspect Test Database State
```typescript
it('debug test', async () => {
// Perform operation
await caller.createMessage({
/* ... */
});
// Print database state
const allMessages = await serverDB.select().from(messages);
console.log('All messages:', allMessages);
});
```
### 2. Use Drizzle Studio
```bash
# Launch Drizzle Studio to inspect test database
pnpm db:studio
```
### 3. Retain Test Data
```typescript
afterEach(async () => {
// Temporarily comment out cleanup to retain data for debugging
// await cleanupTestUser(serverDB, userId);
});
```
## FAQ
### Q: Integration tests are slow. What can I do?
A:
1. Focus on critical paths, avoid over-testing
2. Use `test.concurrent` to run independent tests in parallel
3. Optimize test data setup to avoid redundant creation
### Q: Tests interfere with each other. How to fix?
A:
1. Ensure each test uses a unique userId
2. Thoroughly clean up data in `afterEach`
3. Use transaction isolation (if supported by the database)
### Q: How to test APIs that require authentication?
A: Use `createTestContext(userId)` to create an authenticated context:
```typescript
const caller = messageRouter.createCaller(createTestContext(userId));
```
## References
- [Vitest Documentation](https://vitest.dev/)
- [Drizzle ORM Documentation](https://orm.drizzle.team/)
- [tRPC Testing Guide](https://trpc.io/docs/server/testing)
- [Test Pyramid by Martin Fowler](https://martinfowler.com/articles/practical-test-pyramid.html)
## Contributing
You're welcome to contribute more integration test cases! Please follow the style of existing test files.