mcpField
T
testing-patterns

testing-patterns

github.com/dedalus-erp-pas测试与质量111 次安装
代码仓库

关于 testing-patterns

面向 JavaScript 和 TypeScript 开发者的测试模式技能,涵盖单元、集成和端到端测试策略,并提供模拟、测试组织及防止测试不稳定的指导。

主要功能

  1. 01单元、集成与端到端策略
  2. 02AAA 结构与描述性测试名称
  3. 03模拟、存根与间谍
  4. 04测试数据工厂与组织方式
  5. 05不稳定测试的成因与处理

适用场景

  1. 01规划项目测试策略
  2. 02提升覆盖率与可靠性
  3. 03确定模拟对象的范围
  4. 04排查不稳定或脆弱的测试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dedalus-erp-pas/foundation-skills --skill testing-patterns

安装后在 Claude Code 中输入/testing-patterns确认已加载。

详细介绍

测试模式与策略

JavaScript/TypeScript 应用测试综合指南。作为 tdd 技能(侧重工作流)的补充,提供具体的模式、策略和反模式。

何时使用此技能

在用户遇到以下情况时启用:

  • 想为现有代码或新代码编写测试
  • 需要为项目制定测试策略
  • 想提高测试覆盖率或可靠性
  • 测试不稳定或脆弱
  • 询问有关模拟、桩或测试替身的问题
  • 需要了解单元测试、集成测试和端到端测试的指导

测试金字塔

        /  E2E  \          Few, slow, high confidence
       /----------\
      / Integration \      Moderate count, medium speed
     /----------------\
    /      Unit        \   Many, fast, focused
   /____________________\

**经验法则:**约 70% 单元测试、20% 集成测试、10% 端到端测试。根据项目调整——遗留代码库通常可以先从增加集成测试中获益。

1. 单元测试模式

测试结构:AAA(准备、执行、断言)
describe('calculateDiscount', () => {
  it('should apply 10% discount for orders above 100€', () => {
    // Arrange
    const order = { total: 150, items: ['item1', 'item2'] }

    // Act
    const result = calculateDiscount(order)

    // Assert
    expect(result).toBe(135)
  })
})
命名约定

使用具有描述性的名称,使其读起来像规格说明:

// GOOD — describes behavior
it('should return 401 when token is expired')
it('should retry 3 times before failing')
it('should trim whitespace from patient name')

// BAD — describes implementation
it('should call validateToken')
it('should set retryCount to 3')
it('should use .trim()')
参数化测试(表驱动)
describe('parseHL7Date', () => {
  it.each([
    ['20260315', new Date(2026, 2, 15)],
    ['20260315120000', new Date(2026, 2, 15, 12, 0, 0)],
    ['', null],
    ['invalid', null],
  ])('should parse "%s" to %s', (input, expected) => {
    expect(parseHL7Date(input)).toEqual(expected)
  })
})
错误场景测试

始终测试非正常路径:

describe('PatientService.findById', () => {
  it('should throw NotFoundError for unknown patient ID', async () => {
    await expect(service.findById('UNKNOWN'))
      .rejects.toThrow(NotFoundError)
  })

  it('should throw ValidationError for empty ID', async () => {
    await expect(service.findById(''))
      .rejects.toThrow(ValidationError)
  })
})
异步代码测试
// Promises
it('should resolve with patient data', async () => {
  const patient = await fetchPatient('PAT123')
  expect(patient.name).toBe('DUPONT')
})

// Event emitters
it('should emit "transfer" event on unit change', (done) => {
  emitter.on('transfer', (data) => {
    expect(data.unit).toBe('CARDIO')
    done()
  })
  service.transferPatient('PAT123', 'CARDIO')
})

2. 集成测试模式

集成测试用于验证组件能否正确协同工作。

数据库集成测试
describe('PatientRepository', () => {
  let db: TestDatabase

  beforeAll(async () => {
    db = await TestDatabase.create()  // Real DB, not mock
    await db.migrate()
  })

  afterEach(async () => {
    await db.truncate()  // Clean between tests
  })

  afterAll(async () => {
    await db.destroy()
  })

  it('should persist and retrieve a patient', async () => {
    const repo = new PatientRepository(db.connection)

    await repo.create({
      id: 'PAT123',
      name: 'DUPONT',
      birthDate: '1975-03-15',
    })

    const found = await repo.findById('PAT123')
    expect(found).toMatchObject({
      id: 'PAT123',
      name: 'DUPONT',
    })
  })
})
API 集成测试
describe('POST /api/patients', () => {
  it('should create a patient and return 201', async () => {
    const response = await request(app)
      .post('/api/patients')
      .send({ name: 'DUPONT', birthDate: '1975-03-15' })
      .set('Authorization', `Bearer ${validToken}`)

    expect(response.status).toBe(201)
    expect(response.body.data.id).toBeDefined()
  })

  it('should return 400 for missing required fields', async () => {
    const response = await request(app)
      .post('/api/patients')
      .send({})  // Missing name
      .set('Authorization', `Bearer ${validToken}`)

    expect(response.status).toBe(400)
    expect(response.body.errors).toContainEqual(
      expect.objectContaining({ field: 'name' })
    )
  })

  it('should return 401 without authentication', async () => {
    const response = await request(app)
      .post('/api/patients')
      .send({ name: 'DUPONT' })

    expect(response.status).toBe(401)
  })
})
消息处理集成测试

与医疗消息处理(HPK、HL7)尤其相关:

describe('HPK Message Pipeline', () => {
  it('should parse, validate, and transform an ID|M1 message', async () => {
    const rawMessage = 'ID|M1|C|HEXAGONE|20260122120000|USER001|PAT123|DUPONT|JEAN|19750315|M'

    const result = await pipeline.process(rawMessage)

    expect(result.type).toBe('ID')
    expect(result.code).toBe('M1')
    expect(result.patient.name).toBe('DUPONT')
    expect(result.valid).toBe(true)
  })
})

3. 端到端测试模式

端到端测试通过真实应用验证完整的用户工作流。

页面对象模式
class PatientListPage {
  constructor(private page: Page) {}

  async goto() {
    await this.page.goto('/patients')
  }

  async search(query: string) {
    await this.page.fill('[data-testid="search-input"]', query)
    await this.page.click('[data-testid="search-button"]')
  }

  async getResults() {
    return this.page.locator('[data-testid="patient-row"]').allTextContents()
  }

  async clickPatient(name: string) {
    await this.page.click(`text=${name}`)
  }
}

// Usage in test
test('should find patient by name', async ({ page }) => {
  const patientList = new PatientListPage(page)
  await patientList.goto()
  await patientList.search('DUPONT')

  const results = await patientList.getResults()
  expect(results).toContain('DUPONT JEAN')
})
Data-testid 约定

使用 data-testid 属性作为测试选择器,绝不要使用 CSS 类或元素结构:

// GOOD — stable selector
await page.click('[data-testid="submit-admission"]')

// BAD — brittle, breaks on style/structure changes
await page.click('.btn.btn-primary.submit')
await page.click('form > div:nth-child(3) > button')
视觉回归测试
test('patient dashboard matches snapshot', async ({ page }) => {
  await page.goto('/dashboard')
  await page.waitForLoadState('networkidle')
  await expect(page).toHaveScreenshot('dashboard.png', {
    maxDiffPixelRatio: 0.01,
  })
})

4. 模拟策略

何时使用模拟
模拟不要模拟
外部 HTTP API自己的数据库(使用真实的测试数据库)
时间/日期(以保证确定性)模块间的内部协作对象
文件系统(在实际操作不便时)简单的工具函数
第三方服务(支付、邮件)数据转换
特定于环境的功能业务逻辑
Mock、Stub 与 Spy
// STUB — returns a fixed value
const getPatient = vi.fn().mockResolvedValue({ id: 'PAT123', name: 'DUPONT' })

// SPY — observes calls without changing behavior
const logSpy = vi.spyOn(logger, 'info')
await service.admitPatient(data)
expect(logSpy).toHaveBeenCalledWith('Patient admitted', { id: 'PAT123' })

// MOCK — replaces the entire module
vi.mock('./emailService', () => ({
  sendAdmissionNotification: vi.fn().mockResolvedValue(true),
}))
使用 MSW 模拟 HTTP

优先使用 MSW(Mock Service Worker),而不是手动模拟 fetch:

import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'

const server = setupServer(
  http.get('/api/patients/:id', ({ params }) => {
    return HttpResponse.json({
      id: params.id,
      name: 'DUPONT',
      birthDate: '1975-03-15',
    })
  })
)

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
时间模拟
describe('token expiration', () => {
  beforeEach(() => {
    vi.useFakeTimers()
  })

  afterEach(() => {
    vi.useRealTimers()
  })

  it('should reject expired tokens', () => {
    const token = createToken({ expiresIn: '1h' })

    vi.advanceTimersByTime(2 * 60 * 60 * 1000)  // 2 hours later

    expect(() => validateToken(token)).toThrow('Token expired')
  })
})

5. 测试组织

文件结构——就近放置
src/
├── patients/
│   ├── PatientService.ts
│   ├── PatientService.test.ts      # Unit tests next to source
│   ├── PatientRepository.ts
│   └── PatientRepository.test.ts
├── __tests__/
│   └── integration/
│       ├── patient-admission.test.ts    # Integration tests
│       └── hl7-message-flow.test.ts
└── e2e/
    ├── patient-workflow.spec.ts    # E2E tests
    └── pages/
        └── PatientListPage.ts     # Page objects
测试工具——工厂函数

避免复制粘贴测试数据。使用工厂函数:

// test/factories/patient.ts
export function buildPatient(overrides: Partial<Patient> = {}): Patient {
  return {
    id: `PAT${Math.random().toString(36).slice(2, 8)}`,
    name: 'DUPONT',
    firstName: 'JEAN',
    birthDate: '1975-03-15',
    sex: 'M',
    ...overrides,
  }
}

// Usage
it('should flag underage patients', () => {
  const minor = buildPatient({ birthDate: '2015-06-01' })
  expect(isMinor(minor)).toBe(true)
})

6. 常见反模式

测试实现细节
// BAD — tests internal state
it('should set isLoading to true', () => {
  component.fetchData()
  expect(component.state.isLoading).toBe(true)
})

// GOOD — tests observable behavior
it('should show loading spinner while fetching', async () => {
  render(<PatientList />)
  expect(screen.getByTestId('spinner')).toBeVisible()
  await waitForElementToBeRemoved(screen.queryByTestId('spinner'))
})
过度模拟
// BAD — mocking everything, testing nothing real
it('should create patient', async () => {
  vi.mock('./db')
  vi.mock('./validator')
  vi.mock('./logger')
  // ... what are you even testing?
})

// GOOD — use real implementations, mock only boundaries
it('should create patient in database', async () => {
  const repo = new PatientRepository(testDb)
  const service = new PatientService(repo)  // Real repo, real DB

  const patient = await service.create({ name: 'DUPONT' })
  expect(patient.id).toBeDefined()
})
不稳定测试模式

常见原因及解决方法:

原因解决方法
共享的可变状态使用 beforeEach 重置状态
依赖时间的测试使用 vi.useFakeTimers()
异步测试中的竞态条件使用 waitFor / findBy,而不是 getBy
依赖执行顺序的测试每个测试都必须独立
单元测试中的网络调用使用 MSW 模拟 HTTP
硬编码端口使用动态端口分配
过度使用快照
// BAD — meaningless snapshot of entire component
it('should render', () => {
  const { container } = render(<PatientCard patient={patient} />)
  expect(container).toMatchSnapshot()  // 200 lines of HTML nobody reads
})

// GOOD — targeted assertions
it('should display patient name and birthdate', () => {
  render(<PatientCard patient={patient} />)
  expect(screen.getByText('DUPONT JEAN')).toBeVisible()
  expect(screen.getByText('15/03/1975')).toBeVisible()
})

7. 测试检查清单

提交 PR 前,请确认:

[ ] New feature has tests covering happy path and error cases
[ ] Tests are independent — can run in any order
[ ] No flaky tests introduced (run suite 3x to verify)
[ ] Test names describe behavior, not implementation
[ ] Mocks are limited to external boundaries
[ ] No hardcoded test data — using factories
[ ] Test utilities are shared, not duplicated
[ ] Integration tests clean up after themselves
[ ] E2E tests use data-testid selectors
[ ] Coverage hasn't decreased

框架快速参考

框架最适用场景命令
Vitest单元测试 + 集成测试(Vite 项目)vitest run
Jest单元测试 + 集成测试(遗留项目、CRA)jest --coverage
Playwright端到端测试、视觉回归playwright test
Cypress端到端测试、组件测试cypress run
Testing LibraryDOM 测试(React、Vue)与 Vitest/Jest 搭配使用
MSWHTTP 模拟与任意运行器搭配使用

评论

支持 Markdown