2024-12-08

a minimal test runner

the following class executes test cases were input arguments and expected output can be specified in a compact format. it is a minimalistic coffeescript, and therefore also javascript, library for automatic testing.

example

tests = [
  [
    string_indices

    [["/a////b//c/d","/"]]  # list of input arguments
    [0, 2, 3, 4, 5, 7, 8, 10]  # expected result

    [["abcd","bc"]]  # next input argument list
    [1]  # next expected result, and so on
  ]

  # the next test group and test
  [
    string_multiply
    ["a", 0]
    ""
    ["a", 3]
    "aaa"
  ]
]

the name of the test and the calling "this" context can also be provided by using an array instead of a function..

["name", function]
[this_context, function]

test execution with reporting

test_runner = new test_runner_class
test_runner.execute tests

the customizable default reporting format

string-indices 1 2 3 4 5 6 7 8 9
string-multiply 1 2 3
string-replace-string 1 2 3 4 5 6 7 8 9
string-quote 1
  failure string-quote 2
  inp "t'est"
  exp "\"t'est\"x"
  out "\"t'est\""
  • inp: the arguments to the test-procedure
  • exp: the expected result
  • out: the actual result

instead of writing to standard output, it can also create an array of test results for each group, which could be processed by other tools.

results = test_runner.execute_tests tests

the implementation

class test_runner_class
  is_string: (a) -> typeof a is "string"
  to_json: (a) -> JSON.stringify(a).replace /,(?=\S)/g, ", "
  is_plain_object: (x) -> x? and typeof x is "object" and x.constructor is Object
  object_merge: (a, b) ->
    for k, v of b
      if @is_plain_object(v) and @is_plain_object(a[k])
        a[k] = @object_merge a[k], v
      else
        a[k] = v
    a
  report_compact_failure_strings: (inp, exp, out) -> [((@to_json a for a in inp).join ", "), @to_json(exp), @to_json(out)]
  report_compact: (results) ->
    for [name, test_results...] in results
      process.stdout.write name
      for [status, index, name, inp, exp, out] in test_results
        if status then process.stdout.write " #{index}"
        else
          [inp_string, exp_string, out_string] = @report_compact_failure_strings inp, exp, out
          process.stdout.write [
            "\n  failure #{name} #{index}"
            "inp #{inp_string}"
            "exp #{exp_string}"
            "out #{out_string}"
          ].join "\n  "
    console.log ""
  constructor: (options) ->
    default_options =
      reporter: @report_compact
    @options = @object_merge default_options, options
    @options.reporter = @options.reporter.bind @
  execute_tests: (tests) ->
    status = true
    for [f, rest...] in tests
      break unless status
      [name, context] =
        if Array.isArray f
          [name_or_context, f] = f
          if @is_string(name_or_context) then [name_or_context, null] else [f.name, name_or_context]
        else [f.name, null]
      results = [name]
      for i in [0...rest.length] by 2
        inp = rest[i]
        exp = rest[i + 1]
        out = f.apply context, inp
        out_string = @to_json out
        exp_string = @to_json exp
        status = out_string == exp_string
        results.push [status, i / 2, name, inp, exp, out]
        break unless status
      results
  execute: (tests) -> @options.reporter @execute_tests tests