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

Julia 教程 / Julia 测试与文档

20. 测试与文档

测试和文档是高质量软件的两大支柱。Julia 的 Test 模块和 Documenter.jl 提供了完整的测试与文档解决方案。

20.1 Test 模块基础

@test 宏

using Test

# 基本断言
@test 1 + 1 == 2
@test 2 > 1
@test "hello" == "hello"

# 浮点近似
@test 0.1 + 0.2  0.3
@test sin(π)  0.0 atol=1e-10

# 集合包含
@test 3 in [1, 2, 3, 4]
@test "hello"  "hell"

# 类型检查
@test 1 isa Int
@test [1, 2] isa Vector{Int}

# 异常测试
@test_throws DomainError sqrt(-1)
@test_throws MethodError sin("hello")

@test 失败消息

# 默认失败消息
@test 1 + 1 == 3
# Test Failed: 1 + 1 == 3
#   Expression: 1 + 1 == 3
#    Evaluated: 2 == 3

# 自定义失败消息
x = 42
@test x > 100 "x 应该大于 100,实际值: $x"
# Test Failed: x 应该大于 100,实际值: 42

20.2 @testset 组织测试

基本用法

using Test

@testset "数学运算测试" begin
    @test 1 + 1 == 2
    @test 2 * 3 == 6
    @test 10 / 2  5.0
end

@testset "字符串操作测试" begin
    @test uppercase("hello") == "HELLO"
    @test length("Julia") == 5
    @test startswith("Hello", "He")
end

嵌套测试集

@testset "MyPackage" begin
    @testset "基础功能" begin
        @testset "加法" begin
            @test 1 + 1 == 2
            @test -1 + 1 == 0
        end
        
        @testset "乘法" begin
            @test 2 * 3 == 6
            @test 0 * 100 == 0
        end
    end
    
    @testset "高级功能" begin
        @testset "幂运算" begin
            @test 2^10 == 1024
            @test 0^0 == 1
        end
    end
end

参数化测试

@testset "参数化测试: $f" for f in [sin, cos, tan]
    @test f(0.0)  f(0.0)
    @test isfinite(f(1.0))
end

# 多参数
@testset "测试 a=$a, b=$b" for (a, b) in [(1, 2), (3, 4), (5, 6)]
    @test a + b == b + a
    @test a * b == b * a
end

返回值测试

@testset "返回测试集结果" begin
    ts = @testset "子测试" begin
        @test 1 == 1
        @test 2 == 2
    end
    
    # 检查所有测试是否通过
    @test ts.n_passed == 2
    @test ts.n_failed == 0
end

20.3 测试组织结构

包测试结构

MyPackage/
├── src/
│   └── MyPackage.jl
├── test/
│   ├── runtests.jl      # 入口文件
│   ├── test_math.jl      # 数学功能测试
│   ├── test_io.jl        # IO 功能测试
│   └── test_utils.jl     # 工具函数测试
└── Project.toml

runtests.jl

using MyPackage
using Test

@testset "MyPackage.jl" begin
    include("test_math.jl")
    include("test_io.jl")
    include("test_utils.jl")
end

test_math.jl

@testset "数学功能" begin
    @testset "add" begin
        @test MyPackage.add(1, 2) == 3
        @test MyPackage.add(-1, 1) == 0
    end
    
    @testset "multiply" begin
        @test MyPackage.multiply(3, 4) == 12
        @test MyPackage.multiply(0, 100) == 0
    end
end

运行测试

# 在 REPL 中
using Pkg
Pkg.test("MyPackage")

# 命令行
# julia --project -e 'using Pkg; Pkg.test()'

# 运行特定测试文件
# julia test/runtests.jl

20.4 测试最佳实践

辅助函数

# 测试辅助函数
function test_approx(actual, expected; atol=1e-6)
    @test abs(actual - expected) < atol
end

# 测试异常消息
function test_error_msg(f, msg)
    try
        f()
        @test false "应该抛出异常"
    catch e
        @test occursin(msg, string(e))
    end
end

# 使用
test_approx(sin(π/4), 2/2)
test_error_msg(() -> sqrt(-1), "DomainError")

Mock 与测试替身

# 简单的函数替换测试
function test_with_mock()
    # 保存原始函数
    original_rand = rand
    
    # 替换为确定性函数
    mock_rand() = 0.5
    
    try
        # 使用 mock
        @test mock_rand() == 0.5
    finally
        # 恢复原始(在实际中需要更复杂的方法)
    end
end

测试覆盖率考量

# 测试应覆盖:
# 1. 正常路径(happy path)
# 2. 边界条件
# 3. 错误输入
# 4. 空输入

@testset "边界条件" begin
    @test length(String[]) == 0           # 空输入
    @test sum([1]) == 1                   # 单元素
    @test maximum([1, 2, 3]) == 3         # 最大值在末尾
    @test minimum([3, 2, 1]) == 1         # 最小值在开头
end

20.5 文档字符串(Docstrings)

基本格式

"""
    add(a, b)

计算两个数的和。

# 参数
- `a::Number`: 第一个数
- `b::Number`: 第二个数

# 返回值
- `Number`: 两数之和

# 示例
```julia
julia> add(1, 2)
3

julia> add(1.5, 2.5)
4.0

另见

subtract, multiply """ function add(a::Number, b::Number) return a + b end


### 多方法文档

```julia
"""
    process(data::Vector{Float64})

处理浮点数向量。

    process(data::Vector{Int})

处理整数向量(先转换为浮点数)。

    process(path::String)

从文件读取数据并处理。
"""
function process end

process(data::Vector{Float64}) = sum(data)
process(data::Vector{Int}) = process(Float64.(data))
process(path::String) = process(parse.(Float64, readlines(path)))

Markdown 格式

"""
# 标题

支持完整的 Markdown 语法:

- **粗体** 和 *斜体*
- `代码` 和 ```代码块```
- [链接](https://example.com)
- 列表和表格

## 数学公式

\sum_{i=1}^{n} x_i


!!! warning "警告"
    这是一个重要的注意事项。

!!! note "提示"
    这是有用的提示信息。
"""
function documented_function end

20.6 DocStringExtensions.jl

using DocStringExtensions

# 自动插入模板
"""
$(SIGNATURES)

计算两个数的和。

$(METHODLIST)

$(TYPEDFIELDS)
"""
function advanced_add(a::T, b::T) where T <: Number
    return a + b
end

可用模板

模板含义输出内容
SIGNATURES函数签名自动提取参数列表
METHODLIST方法列表所有方法的签名
TYPEDFIELDS结构体字段字段名和类型
READMEREADME 内容包的 README.md
EXPORTS导出列表所有导出的名称
IMPORTS导入列表使用的包
"""
$(TYPEDFIELDS)
"""
struct Point{T <: Real}
    "x 坐标"
    x::T
    "y 坐标"
    y::T
end

# 文档会自动包含字段说明

20.7 文档生成 (Documenter.jl)

项目结构

MyPackage/
├── src/
│   └── MyPackage.jl
├── docs/
│   ├── src/
│   │   ├── index.md        # 首页
│   │   ├── guide.md        # 使用指南
│   │   ├── api.md          # API 参考
│   │   └── examples.md     # 示例
│   ├── make.jl             # 文档构建脚本
│   └── Project.toml
└── Project.toml

make.jl

using Documenter
using MyPackage

makedocs(
    sitename = "MyPackage.jl",
    modules = [MyPackage],
    pages = [
        "首页" => "index.md",
        "使用指南" => "guide.md",
        "API 参考" => "api.md",
        "示例" => "examples.md",
    ],
    format = Documenter.HTML(
        prettyurls = get(ENV, "CI", nothing) == "true",
        canonical = "https://myuser.github.io/MyPackage.jl",
    ),
)

deploydocs(
    repo = "github.com/myuser/MyPackage.jl.git",
    devbranch = "main",
)

index.md

# MyPackage.jl

MyPackage 是一个用于演示的 Julia 包。

## 安装

```julia
using Pkg
Pkg.add("MyPackage")

快速开始

using MyPackage

result = add(1, 2)
println(result)  # 3

目录

Pages = ["guide.md", "api.md", "examples.md"]

### api.md(API 参考)

```markdown
# API 参考

## 公共函数

```@docs
add
subtract
multiply

类型

Point
Matrix

索引


### 构建文档

```julia
# 本地构建
cd("docs")
julia --project -e 'include("make.jl")'

# 预览
# 在 docs/build/ 目录下打开 index.html

20.8 文档测试(Doctest)

"""
    fibonacci(n)

计算第 n 个斐波那契数。

# 示例
```jldoctest
julia> fibonacci(0)
0

julia> fibonacci(1)
1

julia> fibonacci(10)
55

高精度示例

julia> fibonacci(100)
354224848179261915075

""" function fibonacci(n) n <= 0 && return 0 n == 1 && return 1 a, b = 0, 1 for _ in 2:n a, b = b, a + b end return b end


> 💡 **Doctest 好处**:文档中的示例同时也是测试。如果代码变更导致输出不同,构建文档时会报错。

## 20.9 覆盖率 (Coverage.jl)

### 生成覆盖率报告

```julia
using Coverage

# 收集覆盖率数据(需要先运行测试)
# julia --code-coverage test/runtests.jl

# 处理覆盖率数据
coverage = process_folder("src")

# 生成 LCOV 报告
LCOV.writefile("lcov.info", coverage)

# 生成文本报告
covered, total = get_summary(coverage)
println("覆盖率: $(round(covered/total*100, digits=1))%")

命令行覆盖率

# 生成覆盖率
julia --code-coverage=user test/runtests.jl

# 查看 .cov 文件
cat src/MyPackage.jl.cov

覆盖率目标

覆盖率评价说明
> 90%优秀推荐
80-90%良好可接受
60-80%一般需改进
< 60%较差需大幅补充测试

20.10 基准测试回归

基准测试脚本

# benchmark/run_benchmarks.jl
using BenchmarkTools, MyPackage

const SUITE = BenchmarkGroup()

SUITE["add"] = @benchmarkable add(1, 2)
SUITE["multiply"] = @benchmarkable multiply(3, 4)

# 参数化
SUITE["sum"] = BenchmarkGroup()
for n in [100, 1000, 10000]
    SUITE["sum"]["n=$n"] = @benchmarkable sum(rand($n))
end

# 运行
results = run(SUITE, verbose=true)

PkgBenchmark.jl

using PkgBenchmark

# 运行基准测试
results = benchmarkpkg("MyPackage")

# 与上次结果比较
judgement = judge(results, loadresults("MyPackage"))

# 导出结果
export_markdown("benchmark/results.md", results)

20.11 CI 配置 (GitHub Actions)

.github/workflows/CI.yml

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    name: Julia ${{ matrix.version }} - ${{ matrix.os }}
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        version:
          - '1.10'
          - '1'
        os:
          - ubuntu-latest
          - macOS-latest
          - windows-latest
    steps:
      - uses: actions/checkout@v4
      - uses: julia-actions/setup-julia@v2
        with:
          version: ${{ matrix.version }}
      - uses: julia-actions/cache@v2
      - uses: julia-actions/julia-buildpkg@v1
      - uses: julia-actions/julia-runtest@v1
      - uses: julia-actions/julia-processcoverage@v1
      - uses: codecov/codecov-action@v4
        with:
          files: lcov.info

  docs:
    name: Documentation
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
      - uses: julia-actions/setup-julia@v2
        with:
          version: '1'
      - uses: julia-actions/cache@v2
      - name: Install dependencies
        run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()'
      - name: Build and deploy
        run: julia --project=docs/ docs/make.jl
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

CI 最佳实践

实践说明
多版本测试至少测试 LTS 和最新版
多平台测试Linux、macOS、Windows
缓存依赖使用 julia-actions/cache
覆盖率上传集成 Codecov
文档自动部署main 分支变更自动更新文档

20.12 文档发布 (GitHub Pages)

配置

# docs/make.jl
deploydocs(
    repo = "github.com/user/MyPackage.jl.git",
    devbranch = "main",
    devurl = "dev",
    versions = ["stable" => "v^", "v#.#.#", "dev" => "dev"],
)

GitHub 设置

  1. 进入仓库 Settings → Pages
  2. Source 选择 gh-pages 分支
  3. 文档地址:https://user.github.io/MyPackage.jl/stable/

文档版本

https://user.github.io/MyPackage.jl/stable/   # 稳定版
https://user.github.io/MyPackage.jl/dev/      # 开发版
https://user.github.io/MyPackage.jl/v1.0.0/   # 特定版本

20.13 完整测试示例

# test/runtests.jl
using MyPackage
using Test

@testset "MyPackage.jl" begin
    
    @testset "基础数学" begin
        @testset "add" begin
            @test MyPackage.add(1, 2) == 3
            @test MyPackage.add(-1, 1) == 0
            @test MyPackage.add(0, 0) == 0
            @test MyPackage.add(1.5, 2.5)  4.0
        end
        
        @testset "multiply" begin
            @test MyPackage.multiply(2, 3) == 6
            @test MyPackage.multiply(0, 100) == 0
            @test MyPackage.multiply(-1, 5) == -5
        end
        
        @testset "divide" begin
            @test MyPackage.divide(10, 2)  5.0
            @test_throws DivideError MyPackage.divide(1, 0)
        end
    end
    
    @testset "数据处理" begin
        @testset "空输入" begin
            @test MyPackage.process(Int[]) == 0
        end
        
        @testset "单元素" begin
            @test MyPackage.process([42]) == 42
        end
        
        @testset "大数据" begin
            data = rand(10000)
            @test MyPackage.process(data)  sum(data) atol=1e-10
        end
    end
    
    @testset "参数化: n=$n" for n in [10, 100, 1000]
        @test MyPackage.fibonacci(0) == 0
        @test MyPackage.fibonacci(1) == 1
    end
end

20.14 扩展阅读

资源链接
Julia 官方文档 - Testhttps://docs.julialang.org/en/v1/stdlib/Test/
Documenter.jlhttps://github.com/JuliaDocs/Documenter.jl
DocStringExtensions.jlhttps://github.com/JuliaDocs/DocStringExtensions.jl
Coverage.jlhttps://github.com/JuliaCI/Coverage.jl
PkgBenchmark.jlhttps://github.com/JuliaCI/PkgBenchmark.jl
Aqua.jl (包质量检查)https://github.com/JuliaTesting/Aqua.jl

20.15 本章小结

主题要点
@test基本断言(相等、近似、异常)
@testset组织和嵌套测试
Docstrings函数文档(Markdown + jldoctest)
Documenter.jl自动生成 HTML 文档
Coverage.jl测试覆盖率度量
GitHub ActionsCI 自动测试与文档部署
Doctest文档中的示例即测试