Testing your code

A test is a function that checks your code still does what you meant. You do not install anything to write one, and there is no special syntax.

Write a test

Give a function a name starting with test_ and put an assert in it. That is all a test is:

  fn double(n: integer) -> integer { n * 2 }
  fn test_double_doubles() {
    assert(double(4) == 8, "double(4) should be 8");
  }

assert takes something that should be true, and a message. If it is true, nothing happens. If it is not, the message is what you will read, so write the message for the person who has to fix it — usually you, later.

Run your tests

Point loft at the file:

  $ loft --tests calc.loft
    ok    calc.loft  (2 fns: test_double_doubles, test_double_of_zero)
  test result: ok. 2 passed; 1 file

It finds every function whose name starts with test_, runs it, and names the file and the functions it found. That result line does not end where it does above — there is a bracket after it, which the last section on this page is about.

The underscore is part of the rule. test_double is a test; testify, or a function called exactly test, is not — and nothing will tell you, because the run says ok having never called it. (A camel-case testDouble cannot catch you out: loft refuses that as a function name.)

There is one exception, and it is what makes a plain script runnable. If a file names NO test_ function at all, loft treats every function in it that takes no parameters as something to run. That is how the pages of this reference are checked: they have a main and no tests. Once one test_ appears in a file, it and its siblings are the whole set, and the helpers beside them go back to being helpers.

Give it a directory instead of a file and it looks in every .loft file underneath. Give it nothing and it starts from where you are:

  $ loft --tests greeter
    ok    greeter/tests/greet.loft  (1 fn: test_greet_names_the_person)
  coverage: all 1 functions were entered by these tests
  test result: ok. 1 passed; 1 file

Over a whole directory it also reports coverage — how many of your functions any of the tests actually entered. One file on its own gets no such line, because a file is not the set of functions you were trying to cover.

Run just one test

While you are fixing one thing, run only that one. Put :: and the test's name after the file:

  $ loft --tests calc.loft::test_double_of_zero
    ok    calc.loft  (1 fn: test_double_of_zero)
  test result: ok. 1 passed; 1 file

When a test fails

A failure names the test and shows your message, once per test and once per file, and the run's exit status stops being zero — which is how a build script finds out:

  $ loft --tests broken/failing.loft ; echo "exit $?"
    FAIL  broken/failing.loft::test_double_of_two  —  assertion failed: double(2) should be 5
    FAIL  broken/failing.loft  (1 failed, 0 passed)
  test result: FAILED. 1 failed; 0 passed; 1 total; 1 file
  exit 1

The message is the part you wrote, which is why a vague message like "it works" costs you time and a specific one saves it.

Read the line after the result

The result line ends with a bracket saying what the run did NOT do:

  $ loft --tests calc.loft
  test result: ok. 2 passed; 1 file  [ran on the interpreter only — native not exercised: loft test --native; no [sandbox] policy — admission not exercised]

That is deliberate. A run that says only "ok" cannot tell you whether the other half was checked or never ran at all. Run them the compiled way and the bracket says the opposite:

  $ loft --tests --native calc.loft
  test result: ok. 2 passed; 1 file  [ran on native only — the interpreter not exercised: loft test; no [sandbox] policy — admission not exercised]

Both halves matter, because the two ways of running your program are meant to give the same answer and a test only checks the one it ran.

Once you have a project

When your code grows into a project with a loft.toml file (the next page), tests live in a tests/ folder and you run them with:

  $ cd greeter && loft test
  test result: ok. 1 passed; 1 file
fn double(n: integer) -> integer { n * 2 }

This page is itself run as a test, so the example below really does pass.

fn main() {
  assert(double(4) == 8, "double(4) should be 8");
  assert(double(0) == 0, "double(0) should be 0");

An assert that holds produces no output at all — silence is success.

  println("both checks passed");
}