Testing
The stdlib
using Test
@testset "Money" begin
@test Money(100) + Money(50) == Money(150)
@test Money(0) == zero(Money)
@test_throws ArgumentError Money(-1)
@test_throws "below absolute zero" Celsius(-300) # match the message (1.8+)
@test 0.1 + 0.2 ≈ 0.3
@test isapprox(a, b; atol = 1e-10)
@test_broken known_bug() # must fail; shouts if it starts passing
@test_skip flaky()
@test_logs (:warn, "retrying") do_thing()
@test_nowarn quiet_thing()
@inferred mysum(v) # asserts type stability
end
@testset catches failures, keeps going, and prints a summary table at the end. Nesting works and the summary is nested too.
Parameterised testsets — a for clause on the testset itself, which re-runs the body per value and labels each:
@testset "roundtrip $T" for T in (Int, Float64, Rational{Int})
@test parse_as(T, string(one(T))) == one(T)
end
@testset "$name" for (name, input, expected) in [
("empty", "", 0),
("single", "1", 1),
("many", "1,2,3", 6),
]
@test parse_sum(input) == expected
end
Layout and running
test/
├── Project.toml # test-only dependencies
├── runtests.jl
├── test_parsing.jl
└── test_arithmetic.jl
# test/runtests.jl
using MyPkg, Test
@testset "MyPkg" begin
include("test_parsing.jl")
include("test_arithmetic.jl")
end
Run with ] test (fresh process, uses test/Project.toml), or Pkg.test("MyPkg"). Individual files run directly with julia --project=test test/test_parsing.jl if they’re self-contained.
Test dependencies go in test/Project.toml (modern) or the [extras]/[targets] sections of the root Project.toml (older style; still seen everywhere).
Test items — running one test from the editor
using TestItems
@testitem "addition" begin
@test 1 + 1 == 2
end
@testitem "needs setup" setup=[SharedFixtures] tags=[:slow] begin
@test length(SharedFixtures.data) == 100
end
@testmodule SharedFixtures begin
const data = collect(1:100)
end
Test items can live next to the code they test in src/, and each runs in its own module. The point is editor integration: JETLS and the VSCode extension put a run button on each @testitem, so you execute one test without a full ] test cycle. Run them in bulk with ReTestItems.runtests(), which also parallelises across processes.
If you’re starting a package now, this is the layout to pick.
Package-quality checks
using Aqua
Aqua.test_all(MyPkg)
Catches: method ambiguities, type piracy, unbound type parameters, stale dependencies, missing compat entries, undefined exports. Cheap to add, catches a class of bug that tests never will.
using JET
JET.test_package(MyPkg; target_defined_modules = true)
Static analysis: possible MethodErrors, undefined variables, type errors on paths your tests didn’t exercise. Noisy on first run; worth triaging once.
Doctests
Examples in docstrings, executed as tests:
"""
double(x)
# Examples
```jldoctest
julia> double(2)
4
```
"""
double(x) = 2x
using Documenter
DocMeta.setdocmeta!(MyPkg, :DocTestSetup, :(using MyPkg); recursive = true)
doctest(MyPkg)
Your documentation examples then cannot rot silently, which is worth more than it sounds.
Property-based and fuzz testing
using Supposition # or PropCheck.jl
@check function roundtrip(s = Data.Text(Data.Characters()))
parse_it(render_it(s)) == s
end
Not as mature as StreamData or Hypothesis, but usable for parser/serialiser pairs and numerical invariants.
Coverage
julia --project --code-coverage=user -e 'using Pkg; Pkg.test()'
using Coverage
coverage = process_folder()
covered, total = get_summary(coverage)
In CI, julia-actions/julia-processcoverage plus Codecov is the standard chain.
CI
The julia-actions set of GitHub Actions is the ecosystem default; they also run under Forgejo Actions and Gitea Actions, since those consume GitHub Action definitions.
- uses: julia-actions/setup-julia@v2
with:
version: '1' # '1' = latest 1.x, 'lts', or '1.10'
- uses: julia-actions/cache@v2
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
- uses: julia-actions/julia-processcoverage@v1
Matrix over ['1', 'lts', 'nightly'] and over OS if you have platform-specific code. Allow nightly to fail.
Test-writing notes specific to Julia
@inferredis a test, not a benchmark. Wrapping your core functions in@inferredin the test suite catches type instability regressions automatically, which is the cheapest performance regression test available.- Comparing floats: use
≈orisapproxwith an explicit tolerance.==on floats in a test is almost always a latent failure. - Testing errors: prefer
@test_throws SpecificErrorover@test_throws Exception. - A
@testsetswallows the return value of its body, so don’t rely on it evaluating to anything. - Randomness: seed it (
Random.seed!(42)) inside the testset, or useStableRNGs.jl— the default RNG algorithm is allowed to change between Julia versions, soseed!alone is not reproducible across releases. - Tests run in a separate process from your REPL session, so a passing
] testafter a failing interactive run usually means you had stale state in the REPL, not a flaky test.