mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
* 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>
411 lines
11 KiB
Plaintext
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.
|