# testing

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

This is the Markdown version of https://osl-lang.org/packages/testing. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

```osl
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:

```osl
import "std:testing"

log testing.equal(2 + 2, 4)
log testing.assert("hello".contains("ell"))
log testing.notEqual("a", "b")
```

Output:

```text
true
true
true
```

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:

```osl
import "std:testing"

auto failure = testing.throws(def() ( testing.equal(1, 2) ))
log failure.kind
log failure.message
```

Output:

```text
AssertionError
expected 1 to equal 2
```

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:

```osl
import "std:testing"

auto failure = testing.throws(def() ( testing.assert(1 > 2, "a ticket needs a positive price") ))
log failure.message
```

Output:

```text
a ticket needs a positive price
```

## 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:

```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
```

Output:

```text
true
expected 1 to be within 0.1 of 2
```

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

- [ ] `testing.equal(average([1, 2, 2]), 1.667)`
- [x] `testing.near(average([1, 2, 2]), 1.667, 0.001)`
- [ ] `testing.notEqual(average([1, 2, 2]), 1.667)`

Answer: The real answer is 1.6666..., so it can never equal 1.667. `near` allows for the rounding.

## Null checks

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

```osl
import "std:testing"

log testing.null(null)
log testing.notNull(3)
log testing.throws(def() ( testing.notNull(null) )).message
```

Output:

```text
true
true
expected value not to be null
```

## Code that should fail

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

```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
```

Output:

```text
must be positive
AssertionError
```

For functions that return a `result`, you don't need it: check `.isErr()` and `.unwrapErr().kind`, as [the errors lesson](/learn/errors.md) 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:

```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)
)
```

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

```osl
import "std:testing"

testing.equal(1 + 1, 2)
log "all good"
```

Test blocks are left out of `osl run` and `osl compile`, so they can't change a shipped program. See [the testing lesson](/learn/testing.md) for blocks, loops in tests and `osl test` itself.

**Quiz.** What does `osl test` do with a `.test.osl` file?

- [ ] Compiles it and ignores its output.
- [x] Runs it, and reports a failure if it throws.
- [ ] Only runs the `test` blocks in it.

Answer: A test file is a normal program, so a failed assertion (a throw) is what marks it as failed.

### Exercise: 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.

Starting code:

```osl
import "std:testing"

def checkAge(int age) int (
  if age < 0 throw "age can't be negative"
  return age
)

log checkAge(30)
```

It should print:

```text
age can't be negative
```

Hint 1: `testing.throws(def() ( ... ))` returns the error, which has a `message`.

A solution:

```osl
import "std:testing"

def checkAge(int age) int (
  if age < 0 throw "age can't be negative"
  return age
)

auto failure = testing.throws(def() ( checkAge(-5) ))
log failure.message
```

## API reference

## API reference

Every member of `std:testing`, generated from the source of OSL.

### 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.
