Writing tests

To start writing tests, you only need to write regular typst scripts, no special syntax or annotations are required.

Let's start with the most common type of tests, unit tests. We'll assume you have a normal package directory structure:

<project>
├─ src
│  └─ lib.typ
└─ typst.toml

Unit tests

Unit tests are found in the tests directory of your project (remember that this is where your typst.toml manifest is found).

Let's write our first test, you can run tt add my-test to add a new unit test, this creates a new directory called my-test inside tests and adds a test script and reference document. This test is located in tests/my-test/tests.typ and is the entrypoint script (like a main.typ file). Assuming you passed no extra options to tt add, this test is going to be a persistent unit test, this means that its output will be compared to a reference document which is stored in tests/my-test/ref/ as individual pages.

You could also pass --ephemeral, which means to create a script which creates this document on every test run or --compile-only, which means the test doesn't create any output and is only compiled.

Your project will now look like this:

<project>
├─ src
│  └─ lib.typ
├─ tests
│  └─ my-test
│     ├─ ref
│     │  └─ 1.png
│     └─ test.typ
└─ typst.toml

If you now run

tt run my-test

you should see something along the lines of

  Starting 1 tests (run ID: 4863ce3b-70ea-4aea-9151-b83e25f11c94)
      pass [ 0s  38ms 413µs] my-test
──────────
   Summary [ 0s  38ms 494µs] 1/1 tests run: all 1 passed

This means that the test was run successfully.

Let's edit the test to actually do something, right now it simply contains Hello World. Write something else in there and see what happens:

-Hello World
+Typst is Great!

Once we run Tytanic again we'll see that the test no longer passes:

  Starting 1 tests (run ID: 7cae75f3-3cc3-4770-8e3a-cb87dd6971cf)
      fail [ 0s  44ms 631µs] my-test
           Page 1 had 1292 deviations
           hint: Diff images have been saved at '<project>/test/tests/my-test/diff'
──────────
   Summary [ 0s  44ms 762µs] 1/1 tests run: all 1 failed

Tytanic has compared the reference output from the original Hello World document to the new document and determined that they don't match. It also told you where you can inspect the difference, the <project>/tests/my-test contains a diff directory. You can take a look to see what changed, you can also take a look at the out and ref directories, these contain the output of the current test and the expected reference output respectively.

Well, but this wasn't a mistake, this was a deliberate change. So, let's update the references to reflect that and try again. For this we use the appropriately named update command:

tt update my-test

You should see output similar to

  Starting 1 tests (run ID: f11413cf-3f7f-4e02-8269-ad9023dbefab)
      pass [ 0s  51ms 550µs] my-test
──────────
   Summary [ 0s  51ms 652µs] 1/1 tests run: all 1 passed

and the test should once again pass.

This test is still somewhat arcane, let's actually test something interesting, like the API of your fancy package.

Let's say you have this function inside your src/lib.typ file:

/// Frobnicates a value.
///
/// -> content
#let frobnicate(
  /// The argument to frobnicate, cannot be `none`.
  ///
  /// -> any
  arg
) = {
  assert.ne(type(arg), type(none), message: "Cannot frobnicate `none`!")

  [Frobnicating #arg]
}

Because Tytanic comes with a custom standard library you can catch panics and extract their messages to ensure your code also works in the failure path.

Let's add another test where we check that this function behaves correctly and let's not return any output but instead just check how it behaves with various inputs:

tt add --compile-only frobnicate

You project should now look like this:

<project>
├─ src
│  └─ lib.typ
├─ tests
│  ├─ my-test
│  │  ├─ ref
│  │  │  └─ 1.png
│  │  └─ test.typ
│  └─ frobnicate
│     └─ test.typ
└─ typst.toml

Note that the frobnicate test does not contain any other directories for references. Because this test is within the project root it can access the projects internal files, even if they aren't reachable from the package entrypoint.

Let's import our function and test it:

#import "/src/lib.typ": frobnicate

// Passing `auto` should work:
#frobnicate(auto)

// We could even compare it:
#assert.eq(frobnicate("Strings work!"), [Frobnicate #"Strings work!"])
#assert.eq(frobnicate[Content works!], [Frobnicate Content works!])

// If we pass `none`, then this must panic, otherwise we did something wrong.
#assert-panic(() => frobnicate(none))

// We can also unwrap the panics and inspect their eror message.
// Note that we get an array of strings back if a panic occured, or `none` if
// there was no panic.
#assert.eq(
  catch(() => frobnicate(none)),
  "panicked with: Cannot frobnicate `none`!",
)

The exact interface of this library may change in the future.

See #73.

Template tests

In the future you'll be able to automatically test your templates too, but these are currently unsupported

See #49.

Documentation tests

In the future you'll be able to automatically test your documentation examples too, but these are currently unsupported

See #34.

This should equip you with all the knowledge of how to reliably test your projects, but if you're still curious about all the details check out the reference for tests.