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 | 结构体字段 | 字段名和类型 |
README | README 内容 | 包的 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 设置
- 进入仓库 Settings → Pages
- Source 选择
gh-pages 分支 - 文档地址:
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 扩展阅读
20.15 本章小结
| 主题 | 要点 |
|---|
| @test | 基本断言(相等、近似、异常) |
| @testset | 组织和嵌套测试 |
| Docstrings | 函数文档(Markdown + jldoctest) |
| Documenter.jl | 自动生成 HTML 文档 |
| Coverage.jl | 测试覆盖率度量 |
| GitHub Actions | CI 自动测试与文档部署 |
| Doctest | 文档中的示例即测试 |