testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTesting
测试
DEPENDENCY NOTICE: If you are bootstrapping a new project, read Test Utilities immediately.
依赖说明:如果你正在初始化一个新项目,请立即阅读测试工具文档。
2. Unit Testing (Use Cases)
2. 单元测试(用例)
Unit tests focus on validating business logic. All external dependencies (Repositories) MUST be strictly mocked.
单元测试专注于验证业务逻辑。所有外部依赖(Repositories)必须严格进行Mock。
Assertions on neverthrow
Results
neverthrow对neverthrow结果的断言
Because we use Railway-Oriented Programming:
- Use type narrowing directly: from
assert(result.isOk()).node:assert - If successful, make assertions on .
.value - If failing, make assertions on types:
.error.expect(result.error).toBeInstanceOf(SomeDomainError);
由于我们采用面向铁路编程(Railway-Oriented Programming):
- 直接使用类型收窄:来自的
node:assert。assert(result.isOk()) - 如果成功,对进行断言。
.value - 如果失败,对类型进行断言:
.error。expect(result.error).toBeInstanceOf(SomeDomainError);
Setup and Mocking using vitest
Mock Objects
vitest使用vitest Mock对象进行初始化和Mock
You MUST always create factory functions tailored for the test file to produce strictly typed Mock Repositories that assert boundaries natively. Pass at the beginning of each test where state is validated heavily inside mocks.
expect.hasAssertions()typescript
import { expect, test, describe, vi } from "vitest";
import { CreateFolder, CreateFolderParams } from "./createFolder";
import { FolderRepository } from "../domain/interfaces/folderRepository";
import { ok, err } from "neverthrow";
import assert from "node:assert";
// Example Mock Factory Pattern
function createMockFolderRepo(p?: Partial<FolderRepository>): FolderRepository {
return {
create: vi.fn(async () => ok(undefined)),
exists: vi.fn(async () => ok(false)),
...p,
};
}
describe(CreateFolder.name, () => {
test("successfully creates a folder", async () => {
// 1. Arrange
const mockRepo = createMockFolderRepo({
create: vi.fn(async (params) => {
expect(params.folder.folderName.getValue()).toBe("Test Folder");
return ok(undefined);
})
});
const useCase = new CreateFolder({ folderRepo: mockRepo, ... });
// 2. Act
const result = await useCase.execute({ ... });
// 3. Assert Narrowing
assert(result.isOk()); // Narrows result into Ok
expect(result.value.folderId).toBeDefined();
});
});你必须始终为测试文件创建专属的工厂函数,生成原生支持边界断言的强类型Mock仓库。在每个需要在Mock内部大量验证状态的测试开头,调用。
expect.hasAssertions()typescript
import { expect, test, describe, vi } from "vitest";
import { CreateFolder, CreateFolderParams } from "./createFolder";
import { FolderRepository } from "../domain/interfaces/folderRepository";
import { ok, err } from "neverthrow";
import assert from "node:assert";
// Example Mock Factory Pattern
function createMockFolderRepo(p?: Partial<FolderRepository>): FolderRepository {
return {
create: vi.fn(async () => ok(undefined)),
exists: vi.fn(async () => ok(false)),
...p,
};
}
describe(CreateFolder.name, () => {
test("successfully creates a folder", async () => {
// 1. Arrange
const mockRepo = createMockFolderRepo({
create: vi.fn(async (params) => {
expect(params.folder.folderName.getValue()).toBe("Test Folder");
return ok(undefined);
})
});
const useCase = new CreateFolder({ folderRepo: mockRepo, ... });
// 2. Act
const result = await useCase.execute({ ... });
// 3. Assert Narrowing
assert(result.isOk()); // Narrows result into Ok
expect(result.value.folderId).toBeDefined();
});
});3. Integration Testing (Repositories)
3. 集成测试(仓库)
Integration tests validate that the Domain Repositories correctly translate to DB queries via and handle unique constraints (returning explicitly mapped generic DB errors to specific Domain Errors via ).
drizzle-ormneverthrowCRITICAL: Every single integration test MUST run inside the wrapper (documented in Test Utilities). REEMPLAZA la funcion nativa de Vitest. Provee el entorno de base de datos a través de Testcontainers con , ejecuta la prueba y luego captura y anula la cascada del .
txTesttxTesttestNodePgDatabaseTransactionRollbackErrortypescript
import { describe, expect } from "vitest";
import { txTest } from "@/shared/test/integrationUtils";
import { PostgresFolderRepository } from "./postgresFolderRepository";
// ⚠️ No importar el 'test' estandar de vitest cuando se usa txTest
// Usa type Tx = NodePgDatabase para helpers de creacion si existen
describe(PostgresFolderRepository.name, () => {
txTest("successfully creates a folder and returns ok", async (tx) => {
// txTest wraps the vitest test directly.
// Uses the isolated database connection 'tx'
const repo = new PostgresFolderRepository({ db: tx });
// Pass the transaction down to actual methods
const result = await repo.create({ tx, folder, workspaceId });
expect(result.isOk()).toBe(true);
const exists = await repo.exists({ folderId: folder.getId() });
assert(exists.isOk()); // with node:assert
expect(exists.value).toBe(true);
}); // txTest automatically rolls back via tx.rollback() internally
});集成测试验证领域仓库是否通过正确转换为数据库查询,并处理唯一约束(通过neverthrow将通用数据库错误显式映射为特定领域错误)。
drizzle-orm关键要求:每个集成测试必须在包装器中运行(文档见测试工具)。 替代 Vitest的原生函数。它通过Testcontainers提供带有的数据库环境,执行测试后捕获并回滚。
txTesttxTesttestNodePgDatabaseTransactionRollbackErrortypescript
import { describe, expect } from "vitest";
import { txTest } from "@/shared/test/integrationUtils";
import { PostgresFolderRepository } from "./postgresFolderRepository";
// ⚠️ 使用txTest时不要导入vitest的标准'test'
// 如果存在创建工具,使用type Tx = NodePgDatabase
describe(PostgresFolderRepository.name, () => {
txTest("successfully creates a folder and returns ok", async (tx) => {
// txTest直接包装vitest测试
// 使用隔离的数据库连接'tx'
const repo = new PostgresFolderRepository({ db: tx });
// 将事务传递给实际方法
const result = await repo.create({ tx, folder, workspaceId });
expect(result.isOk()).toBe(true);
const exists = await repo.exists({ folderId: folder.getId() });
assert(exists.isOk()); // with node:assert
expect(exists.value).toBe(true);
}); // txTest会在内部自动通过tx.rollback()回滚
});4. Additional Unit Test Patterns
4. 其他单元测试模式
See for full patterns and annotated examples.
references/unit-tests.mdKey conventions:
- uses
describe`${ClassName.name} use case` - Define shared VOs and entities at the top of — keep setup explicit, avoid abstracting into helper factories
describe - Mock repos with from
createMock*RepoWithAssertions()@/shared/test/utils - Only configure the methods the test needs — unconfigured methods throw if invoked
"This method should not be called" - For use-cases needing many repos, use for the ones not relevant to the test
createNonCallableMockRepos() - Error tests: both +
toBeInstanceOfdouble assertiontoEqual - Error test names:
`should return ${ErrorClass.name} error ...` - Repo error test names:
`should return ${RepositoryError.name} error when ${RepoClass.name} fails` - Instantiate the use-case explicitly in each test — do NOT abstract into a helper
完整模式和带注释的示例请参见。
references/unit-tests.md核心规范:
- 使用
describe格式`${ClassName.name} use case` - 在顶部定义共享值对象(VO)和实体——保持初始化显式,避免抽象到辅助工厂中
describe - 使用中的
@/shared/test/utils来Mock仓库createMock*RepoWithAssertions() - 仅配置测试所需的方法——未配置的方法被调用时会抛出错误
"This method should not be called" - 对于需要多个仓库的用例,对与测试无关的仓库使用
createNonCallableMockRepos() - 错误测试:同时使用+
toBeInstanceOf双重断言toEqual - 错误测试命名:
`should return ${ErrorClass.name} error ...` - 仓库错误测试命名:
`should return ${RepositoryError.name} error when ${RepoClass.name} fails` - 在每个测试中显式实例化用例——不要抽象到辅助函数中
5. Additional Integration Test Patterns
5. 其他集成测试模式
See for full patterns and annotated examples.
references/integration-tests.mdKey conventions:
- uses
describedirectly (no suffix)ClassName.name - Use from
txTest— never import@/shared/test/integrationUtilsfrom vitesttest - Each receives a
txTestthat auto-rollbacks after the testtx - Define alias at file top
type Tx = NodePgDatabase - All helper functions go outside
describe - Use for deep object comparison on retrieved entities
toEqual - Use to confirm exact count on list results
toHaveLength(n) - Test pagination by creating N items and verifying page sizes
完整模式和带注释的示例请参见。
references/integration-tests.md核心规范:
- 直接使用
describe(无后缀)ClassName.name - 使用中的
@/shared/test/integrationUtils——永远不要从vitest导入txTesttest - 每个会接收一个
txTest,测试后自动回滚tx - 在文件顶部定义别名
type Tx = NodePgDatabase - 所有辅助函数定义在外部
describe - 对检索到的实体使用进行深度对象比较
toEqual - 使用确认列表结果的精确数量
toHaveLength(n) - 通过创建N个项目并验证分页大小来测试分页功能
Helper Structure
辅助函数结构
Each integration test file defines two kinds of helpers. All helpers use named params () — never positional arguments.
{ tx, ... }每个集成测试文件定义两种辅助函数。所有辅助函数必须使用命名参数()——绝不使用位置参数。
{ tx, ... }1 — setupParent
(one level above the subject)
setupParent1 — setupParent
(被测实体的上一级)
setupParentCreates the parent entity with all its own FK dependencies internally. Returns only the parent ID. Tests never call raw directly — everything goes through a setup helper.
insertXtypescript
// UserRepository has no parent dependencies, so there's no setupParent.
// FolderRepository's parent is workspace — setupWorkspace creates user + workspace internally.
async function setupWorkspace({ tx }: { tx: Tx }): Promise<{ workspaceId: UUID }> {
const userId = UUID.random();
const workspaceId = UUID.random();
await tx.insert(users).values({ id: userId.getValue(), ... });
await tx.insert(workspaces).values({ id: workspaceId.getValue(), ownerId: userId.getValue(), ... });
return { workspaceId }; // userId never leaves — callers don't need it
}在内部创建包含所有自身外键依赖的父实体。仅返回父ID。测试永远不要直接调用原始的——所有操作都通过初始化辅助函数完成。
insertXtypescript
// UserRepository没有父依赖,因此没有setupParent。
// FolderRepository的父级是workspace——setupWorkspace会在内部创建user + workspace。
async function setupWorkspace({ tx }: { tx: Tx }): Promise<{ workspaceId: UUID }> {
const userId = UUID.random();
const workspaceId = UUID.random();
await tx.insert(users).values({ id: userId.getValue(), ... });
await tx.insert(workspaces).values({ id: workspaceId.getValue(), ownerId: userId.getValue(), ... });
return { workspaceId }; // userId不会暴露——调用者不需要它
}2 — setupSubject
(the entity under test)
setupSubject2 — setupSubject
(被测实体)
setupSubjectCreates the full FK chain down to the subject entity. The parent ID is an optional param — when omitted, a new parent chain is created automatically. This single function covers two roles:
- Noise — call without parent ID: — creates an isolated chain that won't pollute the subject's queries
await setupWorkspace({ tx }) - Sibling — call with an existing parent ID to create multiple subjects under the same parent
typescript
async function setupWorkspace(p: {
tx: Tx;
userId?: UUID; // optional — if absent, a fresh user is created automatically
}): Promise<{ workspace: Workspace; userId: UUID }> {
const userId = p.userId ?? (await setupUser({ tx: p.tx })).userId;
const workspace = buildWorkspace({ ownerId: userId });
await insertWorkspace({ tx: p.tx, workspace });
return { workspace, userId };
}
// Noise (isolated chain):
await setupWorkspace({ tx });
// Siblings under the same user (for testing pagination, list isolation, etc.):
const { userId, workspace: workspace1 } = await setupWorkspace({ tx });
const { workspace: workspace2 } = await setupWorkspace({ tx, userId });
const { workspace: workspace3 } = await setupWorkspace({ tx, userId });The underlying and helpers exist but are considered internal — they're only called from within setup helpers, not directly from tests.
buildXinsertX创建到被测实体的完整外键链。父ID是可选参数——如果省略,会自动创建新的父链。这个单一函数承担两个角色:
- 隔离测试——不传入父ID调用:——创建不会干扰被测实体查询的独立链
await setupWorkspace({ tx }) - 同层级测试——传入已有父ID创建同一父级下的多个被测实体
typescript
async function setupWorkspace(p: {
tx: Tx;
userId?: UUID; // 可选——如果未提供,会自动创建新用户
}): Promise<{ workspace: Workspace; userId: UUID }> {
const userId = p.userId ?? (await setupUser({ tx: p.tx })).userId;
const workspace = buildWorkspace({ ownerId: userId });
await insertWorkspace({ tx: p.tx, workspace });
return { workspace, userId };
}
// 隔离测试(独立链):
await setupWorkspace({ tx });
// 同一用户下的同层级实体(用于测试分页、列表隔离等):
const { userId, workspace: workspace1 } = await setupWorkspace({ tx });
const { workspace: workspace2 } = await setupWorkspace({ tx, userId });
const { workspace: workspace3 } = await setupWorkspace({ tx, userId });底层的和辅助函数存在,但被视为内部函数——仅由初始化辅助函数调用,不直接在测试中调用。
buildXinsertXNamed Params
命名参数
All helpers MUST use named params. This makes call sites self-documenting and avoids positional arg order mistakes.
typescript
// ✓
function buildFolder({ name }: { name?: FolderName } = {}): Folder { ... }
async function insertFolder({ tx, workspaceId, folder }: { tx: Tx; workspaceId: UUID; folder: Folder }): Promise<void> { ... }
await insertFolder({ tx, workspaceId, folder });
// ✗
async function insertFolder(tx: Tx, workspaceId: UUID, folder: Folder): Promise<void> { ... }
await insertFolder(tx, workspaceId, folder); // easy to swap args silently所有辅助函数必须使用命名参数。这会让调用位置自文档化,避免位置参数顺序错误。
typescript
// ✓
function buildFolder({ name }: { name?: FolderName } = {}): Folder { ... }
async function insertFolder({ tx, workspaceId, folder }: { tx: Tx; workspaceId: UUID; folder: Folder }): Promise<void> { ... }
await insertFolder({ tx, workspaceId, folder });
// ✗
async function insertFolder(tx: Tx, workspaceId: UUID, folder: Folder): Promise<void> { ... }
await insertFolder(tx, workspaceId, folder); // 容易在不知情的情况下交换参数Descriptive Variable Names
描述性变量名
Variable names MUST communicate the role in the test, not just the type. The name is documentation.
typescript
// ✓ — intent is clear at a glance
const nonExistentWorkspaceId = UUID.random();
const { userId: userWithoutWorkspace } = await setupUser({ tx });
const { workspace: someoneElseWorkspace } = await setupWorkspace({ tx });
// ✗ — forces reader to infer meaning from usage
const workspaceId = UUID.random();
const userId2 = UUID.random();变量名必须传达其在测试中的角色,而不仅仅是类型。变量名就是文档。
typescript
// ✓ — 一眼就能明确意图
const nonExistentWorkspaceId = UUID.random();
const { userId: userWithoutWorkspace } = await setupUser({ tx });
const { workspace: someoneElseWorkspace } = await setupWorkspace({ tx });
// ✗ — 迫使读者从用法中推断含义
const workspaceId = UUID.random();
const userId2 = UUID.random();Testing exists
false — always two tests
exists测试exists
返回false的场景——务必拆分为两个测试
existsWhen receives two IDs (e.g. , or ), split the "false" case into two separate tests. They catch different bugs:
existsworkspaceId + userIdfolderId + workspaceId// ✓ Two distinct tests:
"should return false when workspace exists but does not belong to the user" // exists globally, wrong owner
"should return false when workspace ID does not exist in the system" // never existed
// ✗ One combined test that only proves one of the two:
"should return false when workspace not found"当接收两个ID(例如或)时,将“返回false”的情况拆分为两个独立测试。它们能捕获不同的bug:
existsworkspaceId + userIdfolderId + workspaceId// ✓ 两个独立测试:
"should return false when workspace exists but does not belong to the user" // 仓库中存在,但不属于当前用户
"should return false when workspace ID does not exist in the system" // 从未存在过
// ✗ 一个合并测试只能验证其中一种情况:
"should return false when workspace not found"Assertions with neverthrow Results
使用neverthrow结果进行断言
You MUST always use for type narrowing before accessing or :
assert().value.errortypescript
// Ok path
assert(result.isOk());
expect(result.value).toEqual(expectedEntity);
// Err path
assert(result.isErr());
expect(result.error).toBeInstanceOf(SpecificError);
expect(result.error).toEqual(new SpecificError({ ... }));在访问或之前,必须始终使用进行类型收窄:
.value.errorassert()typescript
// 成功路径
assert(result.isOk());
expect(result.value).toEqual(expectedEntity);
// 失败路径
assert(result.isErr());
expect(result.error).toBeInstanceOf(SpecificError);
expect(result.error).toEqual(new SpecificError({ ... }));Value Objects in Tests
测试中的值对象
- Create random: ,
UUID.random(),WorkspaceName.random()DomainName.random() - Create from raw value (unwrap for tests):
typescript
const itemsPerPage = PositiveInteger.from(10)._unsafeUnwrap({ withStackTrace: true, }); - Static zero/one factories: ,
PositiveInteger.one()NonNegativeInteger.zero()
- 创建随机值:,
UUID.random(),WorkspaceName.random()DomainName.random() - 从原始值创建(测试中可直接解包):
typescript
const itemsPerPage = PositiveInteger.from(10)._unsafeUnwrap({ withStackTrace: true, }); - 静态零/一工厂函数:,
PositiveInteger.one()NonNegativeInteger.zero()
Imports
导入规范
You MUST always use path aliases. Use explicit imports where applicable.
@/*typetypescript
// Unit tests
import { expect, test, describe, assert, vi } from "vitest";
import { err, ok } from "neverthrow";
import { createMockWorkspaceRepoWithAssertions } from "@/shared/test/utils";
// Integration tests — do NOT import 'test' from vitest
import { assert, describe, expect } from "vitest";
import { txTest } from "@/shared/test/integrationUtils";必须始终使用路径别名。适用时使用显式导入。
@/*typetypescript
// 单元测试
import { expect, test, describe, assert, vi } from "vitest";
import { err, ok } from "neverthrow";
import { createMockWorkspaceRepoWithAssertions } from "@/shared/test/utils";
// 集成测试——不要从vitest导入'test'
import { assert, describe, expect } from "vitest";
import { txTest } from "@/shared/test/integrationUtils";Test Coverage Checklist
测试覆盖检查清单
Unit tests for use-cases
用例单元测试
- Happy path (all repos succeed)
- Each domain error case (not found, already exists, validation)
- Each repository failure ()
RepositoryError - Edge cases (pagination limits, empty results)
- 正常路径(所有仓库执行成功)
- 每个领域错误场景(未找到、已存在、验证失败)
- 每个仓库失败场景()
RepositoryError - 边界场景(分页限制、空结果)
Integration tests for repositories
仓库集成测试
- Basic create / find / list operations
- Pagination (multiple pages, verify sizes and that noise data is excluded)
- Empty list for entity with no children
- Empty list / null when the parent does not exist
- returns
existsfor the happy pathtrue - returns
existswhen the ID does not exist at all in the systemfalse - returns
existswhen the entity exists but belongs to a different parent (if applicable)false - Constraint violations where relevant (duplicate name, FK missing)
- for every public method
RepositoryError
- 基础创建/查找/列表操作
- 分页(多页,验证大小并确认隔离无关数据)
- 无子实体时返回空列表
- 父实体不存在时返回空列表/Null
- 正常路径下返回
existstrue - ID在系统中完全不存在时返回
existsfalse - 实体存在但属于不同父级时返回
exists(如适用)false - 相关约束违例(重复名称、外键缺失)
- 每个公共方法都测试场景
RepositoryError