4.77. kink/test/TEST

Software testing.

Example:

:TEST.require_from('kink/test/')
:CONTROL.require_from('kink/')

# Test collection
:Tests <- TEST.collect_in{
  TEST.test('10 + 20 == 30'){
    :Sum = 10 + 20
    Sum == 30 || raise('got {}'.format(Sum.repr))
  }
  TEST.group('Num.op_mul'){
    TEST.test('10 * 20 == 200'){
      :Prod = 10 * 20
      Prod == 200 || raise('got {}'.format(Prod.repr))
    }
    TEST.test('20 * 30 == 600'){
      :Prod = 20 * 30
      Prod == 600 || raise('got {}'.format(Prod.repr))
    }
  }
}

# Test execution
Tests.each{(:T)
  T.run{(:C)
    C.on_success{ stdout.print_line('OK! [{}]'.format(T.address)) }
    C.on_skip{ stdout.print_line('Skipped [{}]'.format(T.address)) }
    C.on_error{(:Exc)
      stdout.print_line('Failed: {} [{}]'.format(Exc.message T.address))
    }
  }
}
# Output:
#   OK! [10 + 20 == 30]
#   OK! [Num.op_mul; 10 * 20 == 200]
#   OK! [Num.op_mul; 20 * 30 == 600]

Test collection

Test is created by TEST.test. Tests can be grouped by TEST.group, which can be nested.

`test` and `group` must be called within an invocation of TEST.collect_in. Created tests are returned from `collect_in` as a vec.

Test execution

A test can be executed by Test.run. The result of test execution is success, error, or skip.

Actual usecase

Usually, tests are implemented in source files named as *_test.kn, and executed by kink/test/TEST_TOOL module. See kink/test/TEST_TOOL for details.

4.77.1. TEST.test(Name $test_thunk)

`test` registers a test to the current collecting session.

Preconditions:

• `Name` must be a str.

• $test_thunk must be a thunk.

• `test` must be called within invocation of TEST.collect_in.

`Name` becomes the last part of `address` of the registered test.

$test_thunk is the test thunk.

• If $test_thunk returns a value, it is marked as a success.

• If $test_thunk raises an exception, it is marked as an error.

• If TEST.skip is called within $test_thunk, it is marked as skip.

4.77.2. TEST.group(Group_name $thunk)

`group` makes a group of tests.

`group` calls $thunk. Tests registered within the invocation of $thunk become members of the group.

Invocation of `group` can be nested. It makes nested groups.

Preconditions:

• `Group_name` must be a str.

• $thunk must be a thunk.

• `group` must be called within invocation of TEST.collect_in.

`Group_name` becomes a part of `address` of member tests.

Note that there is no data type which represents a group. Groups are used just to make `address` of tests.

4.77.3. TEST.have_label?(Label)

`have_label?` returns whether the test label set has the specified label.

Preconditions:

• `have_label?` must be called within invocation of a test thunk.

• `Label` must be a str.

See TEST.skip for an example.

4.77.4. TEST.skip

`skip` marks the current test skipped, and terminates invocation of the test thunk by breaking (see CONTROL.with_break).

Precondition:

• `skip` must be called within invocation of a test thunk.

Example:

# example_test.kn

:TEST.require_from('kink/test/')
:RUNTIME.require_from('kink/')

TEST.test('Windows specific test'){
  RUNTIME.windows? || TEST.skip

  :Result = _call_win_api
  Result == 'ok' || raise('got {}'.format(Result.repr))
}

:_call_win_api <- { 'ok' }

TEST.test('Heavy test'){
  TEST.have_label?('heavy') || TEST.skip

  :Result = _do_heavy_calculation
  Result == 42 || raise('got {}'.format(Result.repr))
}

:_do_heavy_calculation <- { 42 }

# On a unix system:

# $ kink mod:kink/test/TEST_TOOL example_test.kn --verbose
# example_test.kn; Windows specific test : Skipped
# example_test.kn; Heavy test : Skipped
#
# Success 0 Failure 0 Skipped 2

# $ kink mod:kink/test/TEST_TOOL example_test.kn --verbose --label heavy
# example_test.kn; Windows specific test : Skipped
# example_test.kn; Heavy test : Success
#
# Success 1 Failure 0 Skipped 1

4.77.5. TEST.collect_in($thunk)

`collect_in` calls $thunk, and returns a vec of tests created during invocation of $thunk.

Precondition:

• `collect_in` must not be called during invocation of `collect_in`.

4.77.6. type test

A test case.

4.77.6.1. Test.run(...[$config_fun={}])

`run` calls the test thunk.

Precondition:

• $config_fun must be a fun which takes `run_config`.

Result:

• If the test thunk returns without raising an exception, or being skipped by TEST.skip, `run` tail-calls the success cont.

• If the test thunk raises an exception, `run` tail-calls the error cont with the exception.

• If TEST.skip is called during the invocation of the test thunk, `run` tail-calls the skip cont.

4.77.6.2. Test.address_parts

`address_parts` returns a vec of the names of the groups and the test.

The group names appear from shallower ones to deeper ones. The test name appears as the last part.

In the next program, the test named “returns 42” is in the group “FOO.bar”, which is in the super group “mod FOO”. Therefore, Test.address_parts returns ["mod FOO" "FOO.bar" "returns 42"].

:Tests = TEST.collect_in{
  TEST.group('mod FOO'){
    TEST.group('FOO.bar'){
      TEST.test('returns 42'){
        :Result = FOO.bar
        Result == 42 || raise('got {}'.format(Result.repr))
      }
    }
  }
}
:Test = Tests.front
stdout.print_line(Test.address_parts.repr) # => ["mod FOO" "FOO.bar" "returns 42"]

4.77.6.3. Test.address

`address` returns the address representation of this test.

It concatenates the groups and the name, such as "mod FOO; FOO.bar; returns 42".

4.77.6.4. Test.repr

`repr` returns the str representation of the test, such as "(test mod FOO; FOO.bar; returns 42)".

4.77.7. TEST.is?(Val)

`is?` returns whether `Val` is a test.

4.77.8. type run_config

The config val type of Test.run.

4.77.8.1. C.labels(Labels)

`labels` sets `Labels` as the test label set of the test thunk invocation.

Precondition:

• `Labels` must be a set of strs.

If `labels` is not called, an empty set is used as the test label set.

4.77.8.2. C.on_success($success_cont)

`on_success` sets $success_cont as the success cont.

Precondition:

• $success_cont must be a thunk.

If `on_success` is not called, a NOP thunk {} is used as the success cont.

4.77.8.3. C.on_error($error_cont)

`on_error` sets $error_cont as the error cont.

Precondition:

• $error_cont must be a fun which takes an exception.

If `on_error` is not called, a fallback function is used. The fallback function raises the exception passed as the parameter.

4.77.8.4. C.on_skip($skip_cont)

`on_skip` sets $skip_cont as the skip cont.

Precondition:

• $skip_cont must be a thunk.

If `on_skip` is not called, a NOP thunk {} is used as the skip cont.