testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Testing

测试

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结果的断言

Because we use Railway-Oriented Programming:
  • Use type narrowing directly:
    assert(result.isOk())
    from
    node:assert
    .
  • If successful, make assertions on
    .value
    .
  • If failing, make assertions on
    .error
    types:
    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 Mock对象进行初始化和Mock

You MUST always create factory functions tailored for the test file to produce strictly typed Mock Repositories that assert boundaries natively. Pass
expect.hasAssertions()
at the beginning of each test where state is validated heavily inside mocks.
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
drizzle-orm
and handle unique constraints (returning explicitly mapped generic DB errors to specific Domain Errors via
neverthrow
).
CRITICAL: Every single integration test MUST run inside the
txTest
wrapper (documented in Test Utilities).
txTest
REEMPLAZA la funcion nativa
test
de Vitest. Provee el entorno de base de datos a través de Testcontainers con
NodePgDatabase
, ejecuta la prueba y luego captura y anula la cascada del
TransactionRollbackError
.
typescript
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
});
集成测试验证领域仓库是否通过
drizzle-orm
正确转换为数据库查询,并处理唯一约束(通过neverthrow将通用数据库错误显式映射为特定领域错误)。
关键要求:每个集成测试必须在
txTest
包装器中运行(文档见测试工具)。
txTest
替代 Vitest的原生
test
函数。它通过Testcontainers提供带有
NodePgDatabase
的数据库环境,执行测试后捕获并回滚
TransactionRollbackError
typescript
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
references/unit-tests.md
for full patterns and annotated examples.
Key conventions:
  • describe
    uses
    `${ClassName.name} use case`
  • Define shared VOs and entities at the top of
    describe
    — keep setup explicit, avoid abstracting into helper factories
  • Mock repos with
    createMock*RepoWithAssertions()
    from
    @/shared/test/utils
  • Only configure the methods the test needs — unconfigured methods throw
    "This method should not be called"
    if invoked
  • For use-cases needing many repos, use
    createNonCallableMockRepos()
    for the ones not relevant to the test
  • Error tests: both
    toBeInstanceOf
    +
    toEqual
    double assertion
  • 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`
    格式
  • describe
    顶部定义共享值对象(VO)和实体——保持初始化显式,避免抽象到辅助工厂中
  • 使用
    @/shared/test/utils
    中的
    createMock*RepoWithAssertions()
    来Mock仓库
  • 仅配置测试所需的方法——未配置的方法被调用时会抛出
    "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
references/integration-tests.md
for full patterns and annotated examples.
Key conventions:
  • describe
    uses
    ClassName.name
    directly (no suffix)
  • Use
    txTest
    from
    @/shared/test/integrationUtils
    — never import
    test
    from vitest
  • Each
    txTest
    receives a
    tx
    that auto-rollbacks after the test
  • Define
    type Tx = NodePgDatabase
    alias at file top
  • All helper functions go outside
    describe
  • Use
    toEqual
    for deep object comparison on retrieved entities
  • Use
    toHaveLength(n)
    to confirm exact count on list results
  • Test pagination by creating N items and verifying page sizes
完整模式和带注释的示例请参见
references/integration-tests.md
核心规范:
  • describe
    直接使用
    ClassName.name
    (无后缀)
  • 使用
    @/shared/test/integrationUtils
    中的
    txTest
    ——永远不要从vitest导入
    test
  • 每个
    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 (
{ tx, ... }
) — never positional arguments.
每个集成测试文件定义两种辅助函数。所有辅助函数必须使用命名参数
{ tx, ... }
)——绝不使用位置参数。

1 —
setupParent
(one level above the subject)

1 —
setupParent
(被测实体的上一级)

Creates the parent entity with all its own FK dependencies internally. Returns only the parent ID. Tests never call
insertX
raw directly — everything goes through a setup helper.
typescript
// 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。测试永远不要直接调用原始的
insertX
——所有操作都通过初始化辅助函数完成。
typescript
// 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)

2 —
setupSubject
(被测实体)

Creates 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:
    await setupWorkspace({ tx })
    — creates an isolated chain that won't pollute the subject's queries
  • 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
buildX
and
insertX
helpers exist but are considered internal — they're only called from within setup helpers, not directly from tests.
创建到被测实体的完整外键链。父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 });
底层的
buildX
insertX
辅助函数存在,但被视为内部函数——仅由初始化辅助函数调用,不直接在测试中调用。

Named 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
返回false的场景——务必拆分为两个测试

When
exists
receives two IDs (e.g.
workspaceId + userId
, or
folderId + workspaceId
), split the "false" case into two separate tests. They catch different bugs:
// ✓ 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"
exists
接收两个ID(例如
workspaceId + userId
folderId + workspaceId
)时,将“返回false”的情况拆分为两个独立测试。它们能捕获不同的bug:
// ✓ 两个独立测试:
"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
assert()
for type narrowing before accessing
.value
or
.error
:
typescript
// 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
.error
之前,必须始终使用
assert()
进行类型收窄:
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
type
imports where applicable.
typescript
// 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";
必须始终使用
@/*
路径别名。适用时使用显式
type
导入。
typescript
// 单元测试
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
  • exists
    returns
    true
    for the happy path
  • exists
    returns
    false
    when the ID does not exist at all in the system
  • exists
    returns
    false
    when the entity exists but belongs to a different parent (if applicable)
  • Constraint violations where relevant (duplicate name, FK missing)
  • RepositoryError
    for every public method
  • 基础创建/查找/列表操作
  • 分页(多页,验证大小并确认隔离无关数据)
  • 无子实体时返回空列表
  • 父实体不存在时返回空列表/Null
  • 正常路径下
    exists
    返回
    true
  • ID在系统中完全不存在时
    exists
    返回
    false
  • 实体存在但属于不同父级时
    exists
    返回
    false
    (如适用)
  • 相关约束违例(重复名称、外键缺失)
  • 每个公共方法都测试
    RepositoryError
    场景