This is the core library that provides basic infrastructure for the
nest
testing eco system. The concept is similar to the Haskell library
tasty
,
however unlike with tasty
the test ecosystem was developed around the
core library instead of providing adaptors to already existing frameworks.
This document provides information on how to write tests using nest
as well as extending it.
Everything in nest
is based around three types:
Nest.Core.TestTree
, this one allows you to specify the hierarchy of testsNest.Core.IsTest
, this one is a type class which is implemented by other libraries likenest-unit
. It allows you to write many different tests like unit tests, property tests, golden unit tests etc. using the same framework.Nest.Core.TestProcessor
, this one specifies how to execute aTestTree
, a default executor to just run all of the tests and print their results in the console is available fromnest-core
.
Since TestTree
is provided by nest-core
you only need two things:
- a
TestProcessor
if you are not content with the built-in one - an
IsTest
implementation, this is provided by external libraries likenest-unit
Assuming you have decided to use nest-unit
here is a full example:
import NestCore
import NestUnit
open Nest.Core
open Nest.Unit
def fileRes (path : System.FilePath) (mode : IO.FS.Mode) : ResourceSpec IO.FS.Handle where
get := IO.FS.Handle.mk path mode
release handle := handle.flush
description := s!"A file handle to {path}"
def tests : TestTree := [nest|
group "Self Tests"
group "Basic"
test "succeeds on true" : UnitTest := do
assert true
test "fails on false (expected to fail)" : UnitTest := do
assert false
group "Resource based"
with resource fileRes "/dev/zero" .read as res
test "assertion 3" : UnitTest := do
let data ← res.read 12
assert <| data.size = 12
group "Option based"
with options fun x => x.insert `Hello "foo"
with options as x
test "assertion 4" : UnitTest := do
assert <| x.contains `Hello
]
def main : IO UInt32 := Nest.Core.defaultMain tests
As you can see nest-core
provides a scoped syntax extension to write
a TestTree
. If you wish to write your own TestTree
without this
extension this is perfectly possible as well since the syntax is just a
very minimal layer on top of the constructors:
def tests : TestTree :=
.group "Self Tests" [
.group "Basics" [
.single "succeeds on true" (assert true),
.single "fails on false" (assert false)
],
.group "Resource based" [
.withResource (fileRes "/dev/zero" .read) fun res =>
.unitTest "assertion 3" do
let data ← res.read 12
assert <| data.size = 12
],
.group "Option based" [
.withOptions (fun x => x.insert `Hello "foo") <|
.getOptions fun x =>
.single "assertion 4" (assert <| x.contains `Hello)
]
]
Besides just basic groups and tests nest-core
supports two further primitives
as seen above:
- Resources, these allow you to work with external resources. The above
example is rather artificial but one could imagine for example a connection
to a (mock) database, auto generated fake data etc. here. Note that
nest-core
guarantees that therelease
function is going to be called. - Options, they are a
Lean.KVMap
and can be both modified and read by theTestTree
, we do plan on eventually allowing to automatically parse these from CLI but at the moment only manual entries can be made.
The defaultMain
processor is going to use the default console based
test runner to then execute your tests.
As explained above there are two points of interest for extending nest
.
Adding new ways to test comes down to writing a new implementation of
the IsTest
type class. The class itself is quite simple:
class IsTest (t : Type) where
run : Options → t → IO Result
As you can see a test run does have access to the Options
and some
arbitrary data , usually some type that represents the property we wish
to test. In order to test the property it is allowed to run arbitrary
computation in IO
and finally return a Result
which is a structure
describing the outcome.
If you want to change the way that tests are executed, for example providing
a parallel or even distributed runner, one that only prints the results
to files etc. you need to write a TestProcessor
. They are also quite
basic in structure:
structure TestProcessor where
relevantOptions : List Lean.Name
shouldRun? : Options → Bool
exec : Options → TestTree → IO UInt32
The
relevantOptions
fields tellsnest
which keys inOptions
you are interested inshouldRun?
field is called with the provided options and returns whether to use this processor or notexec
field is used to actually run aTestTree
and provides an exit code via theUInt32
return value.
In order to then inject your own TestProcessor
into the framework you
want to use defaultMainWithTestProcessor
instead of defaultMain
from above.