强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Nim 完全指南 / 19 测试与质量

第 19 章:测试与质量

19.1 unittest 模块

Nim 内置 unittest 模块,提供完整的测试框架:

import std/unittest

# 基本测试
suite "数学运算":
  test "加法":
    check(1 + 1 == 2)
  
  test "减法":
    check(5 - 3 == 2)
  
  test "乘法":
    check(3 * 4 == 12)
  
  test "浮点比较":
    check(abs(0.1 + 0.2 - 0.3) < 1e-10)

# 多个断言
suite "字符串操作":
  test "长度":
    let s = "Hello"
    check s.len == 5
  
  test "包含":
    check "Hello World".contains("World")
    check "Hello" in "Hello World"
  
  test "拼接":
    check "Hello" & " " & "World" == "Hello World"

19.1.1 断言宏

import std/unittest

suite "断言示例":
  test "check 宏":
    let x = 42
    check x > 0
    check x mod 2 == 0
  
  test "require(失败时终止)":
    let config = loadConfig()
    require config != nil
  
  test "expect(期望异常)":
    expect ValueError:
      discard parseInt("abc")
  
  test "自定义失败消息":
    let result = compute(10)
    check result > 0
    # 失败时会显示 result 的值

19.1.2 测试夹具

import std/unittest

suite "数据库测试":
  setup:
    # 每个测试前执行
    let db = openTestDb()
  
  teardown:
    # 每个测试后执行
    db.close()
  
  test "插入记录":
    db.exec("INSERT INTO users (name) VALUES (?)", "Alice")
    check db.count("users") == 1
  
  test "查询记录":
    db.exec("INSERT INTO users (name) VALUES (?)", "Bob")
    let name = db.getValue("SELECT name FROM users WHERE name = ?", "Bob")
    check name == "Bob"

19.2 测试组织

19.2.1 项目测试结构

myproject/
├── src/
│   ├── myproject.nim
│   └── myproject/
│       ├── utils.nim
│       └── models.nim
├── tests/
│   ├── test_utils.nim
│   ├── test_models.nim
│   └── test_integration.nim
└── myproject.nimble

19.2.2 nimble 任务

# myproject.nimble
task test, "Run all tests":
  exec "nim c -r tests/test_utils.nim"
  exec "nim c -r tests/test_models.nim"

task test_integration, "Run integration tests":
  exec "nim c -r tests/test_integration.nim"

19.2.3 测试示例

# tests/test_utils.nim
import std/unittest
import ../src/myproject/utils

suite "工具函数测试":
  test "add 函数":
    check add(1, 2) == 3
    check add(-1, 1) == 0
    check add(0, 0) == 0
  
  test "multiply 函数":
    check multiply(3, 4) == 12
    check multiply(0, 100) == 0
  
  test "较大值":
    check max(5, 10) == 10
    check max(10, 5) == 10
    check max(5, 5) == 5

19.3 Mock 与 Stub

import std/unittest

# 使用 proc 类型实现 Mock
type
  Logger = proc(msg: string)
  UserService = object
    logger: Logger

proc newUserService(logger: Logger): UserService =
  UserService(logger: logger)

proc createUser(svc: UserService, name: string): bool =
  if name.len == 0:
    svc.logger("Error: empty name")
    return false
  svc.logger("Created user: " & name)
  return true

suite "UserService 测试":
  test "成功创建用户":
    var logs: seq[string]
    let mockLogger: Logger = proc(msg: string) =
      logs.add(msg)
    
    let svc = newUserService(mockLogger)
    check svc.createUser("Alice")
    check logs.len == 1
    check logs[0] == "Created user: Alice"
  
  test "空名字失败":
    var logs: seq[string]
    let mockLogger: Logger = proc(msg: string) =
      logs.add(msg)
    
    let svc = newUserService(mockLogger)
    check not svc.createUser("")
    check logs[0] == "Error: empty name"

19.4 属性测试

import std/unittest, std/random

# 简化的属性测试
template property(name: string, iterations: int = 100, body: untyped) =
  test name:
    for i in 0..<iterations:
      body

suite "属性测试":
  property "排序后长度不变", 100:
    var data = newSeq[int](rand(1..100))
    for i in 0..<data.len:
      data[i] = rand(-1000..1000)
    let originalLen = data.len
    data.sort()
    check data.len == originalLen
  
  property "排序后有序", 100:
    var data = newSeq[int](rand(1..100))
    for i in 0..<data.len:
      data[i] = rand(-1000..1000)
    data.sort()
    for i in 1..<data.len:
      check data[i-1] <= data[i]
  
  property "反转两次等于原序列", 100:
    var data = newSeq[int](rand(1..50))
    for i in 0..<data.len:
      data[i] = rand(100)
    let original = data
    data.reverse()
    data.reverse()
    check data == original

19.5 集成测试

import std/unittest, std/os, std/httpclient

# 集成测试需要真实的服务运行
suite "API 集成测试":
  let client = newHttpClient()
  let baseUrl = "http://localhost:8080"
  
  test "健康检查":
    let response = client.getContent(baseUrl & "/health")
    check response == "OK"
  
  test "创建用户":
    let payload = """{"name": "Test User", "email": "[email protected]"}"""
    let response = client.postContent(baseUrl & "/api/users", payload)
    let data = parseJson(response)
    check data.hasKey("id")
  
  test "获取用户列表":
    let response = client.getContent(baseUrl & "/api/users")
    let data = parseJson(response)
    check data["total"].getInt() >= 0

19.6 nimcheck 静态分析

# 基本检查
nim check src/main.nim

# 检查整个项目
nim check --project src/main.nim

# 风格检查
nim check --hints:on --warnings:on src/main.nim

19.7 性能测试

import std/[unittest, times, strformat]

template benchmark(name: string, iterations: int, body: untyped) =
  let start = cpuTime()
  for i in 0..<iterations:
    body
  let elapsed = cpuTime() - start
  echo &"{name}: {elapsed*1000:.2f} ms ({iterations} iterations)"

suite "性能基准":
  test "排序性能":
    benchmark "快速排序", 1000:
      var data = newSeq[int](1000)
      for j in 0..<data.len:
        data[j] = rand(10000)
      data.sort()
  
  test "字符串拼接":
    benchmark "add 方法", 100000:
      var s = ""
      for j in 0..99:
        s.add("x")
    
    benchmark "& 运算符", 100000:
      var s = ""
      for j in 0..99:
        s = s & "x"

19.8 代码覆盖率

# 使用 nim 编译标志生成覆盖率信息
nim c --passC:"-fprofile-arcs -ftest-coverage" -r tests/test_utils.nim

# 生成报告
gcov tests/test_utils.nim

19.9 CI/CD 集成

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        nim-version: ['2.0.x', 'stable']
    
    steps:
      - uses: actions/checkout@v4
      - uses: jiro4989/setup-nim-action@v1
        with:
          nim-version: ${{ matrix.nim-version }}
      
      - name: Install dependencies
        run: nimble install -d
      
      - name: Run tests
        run: nimble test
      
      - name: Build
        run: nimble build

19.10 实战示例

🏢 完整测试套件

# tests/test_calculator.nim
import std/unittest
import ../src/calculator

suite "Calculator":
  setup:
    var calc = newCalculator()
  
  test "初始值为0":
    check calc.result() == 0
  
  test "加法":
    calc.add(10)
    calc.add(20)
    check calc.result() == 30
  
  test "减法":
    calc.add(100)
    calc.subtract(30)
    check calc.result() == 70
  
  test "乘法":
    calc.add(5)
    calc.multiply(6)
    check calc.result() == 30
  
  test "除法":
    calc.add(100)
    calc.divide(4)
    check calc.result() == 25
  
  test "除以零":
    calc.add(10)
    expect DivByZeroDefect:
      calc.divide(0)
  
  test "重置":
    calc.add(100)
    calc.reset()
    check calc.result() == 0
  
  test "链式操作":
    check calc.add(10).multiply(2).subtract(5).result() == 15

本章小结

功能用途命令
unittest单元测试nim c -r tests/test.nim
check断言check condition
require必须通过require condition
expect期望异常expect ExceptionType:
suite测试套件suite "name":
nimcheck静态分析nim check file.nim

练习

  1. 为之前编写的模块编写完整的单元测试
  2. 使用属性测试验证排序算法的正确性
  3. 设置 GitHub Actions 自动化测试

扩展阅读


上一章:Web 开发 | 下一章:容器化部署