Packages

testing

Check your code with assertions that name what went wrong, and learn how they fit with test blocks, test files and osl test.

import "std:testing"

std:testing has the assertions: small functions that return true when something holds and throw an AssertionError that names both sides when it doesn't. Use them in test files, and in test ( ... ) blocks when a bare comparison doesn't say enough.

Your first assertion#

An assertion that passes returns true. One that fails throws, which stops the program:

main.osl
import "std:testing"

log testing.equal(2 + 2, 4)
log testing.assert("hello".contains("ell"))
log testing.notEqual("a", "b")
Click the code to edit it

Change 4 to 5 and run it again. The failure says expected 4 to equal 5.

Seeing a failure#

A failed assertion ends the program, so to look at one without stopping, wrap it in testing.throws. It runs a function, checks that the function throws, and gives you the error:

main.osl
import "std:testing"

auto failure = testing.throws(def() ( testing.equal(1, 2) ))
log failure.kind
log failure.message
Click the code to edit it

Try testing.equal({a: [1, 2]}, {a: [1, 3]}). Arrays and objects compare by content, and the message prints both whole.

Adding your own message#

Every assertion takes an optional last argument that replaces the default message. Write what you were checking, so a failure reads like a sentence:

main.osl
import "std:testing"

auto failure = testing.throws(def() ( testing.assert(1 > 2, "a ticket needs a positive price") ))
log failure.message
Click the code to edit it

Floating-point numbers#

0.1 + 0.2 is not exactly 0.3 in any language that uses floating point. testing.near(actual, expected, tolerance) accepts a small difference:

main.osl
import "std:testing"

log testing.near(0.1 + 0.2, 0.3, 0.0001)

auto failure = testing.throws(def() ( testing.near(1, 2, 0.1) ))
log failure.message
Click the code to edit it
Quick check

You compare average([1, 2, 2]) to 1.667. Which check is right?

Null checks#

testing.null(value) and testing.notNull(value) check for null:

main.osl
import "std:testing"

log testing.null(null)
log testing.notNull(3)
log testing.throws(def() ( testing.notNull(null) )).message
Click the code to edit it

Code that should fail#

testing.throws is also how you check that a function rejects bad input. It fails when the function returns normally:

main.osl
import "std:testing"

def positive(int n) int (
  if n <= 0 throw "must be positive"
  return n
)

auto failure = testing.throws(def() ( positive(-1) ))
log failure.message

auto missed = testing.throws(def() ( testing.throws(def() -> 1) ))
log missed.kind
Click the code to edit it

For functions that return a result, you don't need it: check .isErr() and .unwrapErr().kind, as the errors lesson shows.

testing.fail(message) throws unconditionally. Put it on a branch that must never run.

In test blocks and test files#

osl test runs two kinds of test. A test ( ... ) block checks every boolean line, and the assertions here are for when you want your own message:

main.osl
import "std:testing"

def average(number[] values) number -> values.sum() / values.len

test (
  testing.equal(average([2, 4]), 3, "average of two values")
  testing.near(average([1, 2, 2]), 1.667, 0.001)
)
Click the code to edit it

A file ending in .test.osl is an ordinary program that osl test runs and fails if it throws:

main.osl
import "std:testing"

testing.equal(1 + 1, 2)
log "all good"
Click the code to edit it

Test blocks are left out of osl run and osl compile, so they can't change a shipped program. See the testing lesson for blocks, loops in tests and osl test itself.

Quick check

What does osl test do with a .test.osl file?

Your turn

Reject a bad age

checkAge should throw for a negative age. Use testing.throws to catch the error from checkAge(-5), and print its message.

main.osl
Ctrl Enter runs
Expected output
age can't be negative

API reference#

10 members, generated from the std:testing source of OSL 9.3.0.

Functions

#testing.assert(boolean condition, any ...message) boolean

assert checks that condition is true. Throws AssertionError ("expected condition to be true" unless message is given) when it is false.

#testing.equal(any actual, any expected, any ...message) boolean

equal checks that actual equals expected, comparing arrays and objects by content. Throws AssertionError naming both values when they differ.

#testing.fail(string message) boolean

fail throws AssertionError with message, for branches a test should never reach.

#testing.isNull(any value, any ...message) boolean

Deprecated. Deprecated: use testing.null(value) instead; is* names are predicates that never throw.

isNull checks that value is null and throws AssertionError when it isn't.

#testing.near(number actual, number expected, number tolerance, any ...message) boolean

near checks that actual is within tolerance of expected, for comparing floating-point results. Throws AssertionError when it isn't (or either number is NaN), and ValueError when tolerance is negative.

#testing.notEqual(any actual, any expected, any ...message) boolean

notEqual checks that actual differs from expected, comparing arrays and objects by content. Throws AssertionError when they are equal.

#testing.notNull(any value, any ...message) boolean

notNull checks that value is not null. Throws AssertionError when it is.

#testing.null(any value, any ...message) boolean

null checks that value is null. Throws AssertionError naming the value when it isn't.

#testing.panics(any fn, any ...message) boolean

Deprecated. Deprecated: use testing.throws(fn) instead, which also returns the error.

panics checks that fn throws and returns true.

#testing.throws(any fn, any ...message) error

throws calls fn with no arguments, checks that it throws, and returns the error it threw so its kind and message can be checked too. Throws AssertionError when fn returns normally and TypeError when fn isn't a function.