Giter VIP home page Giter VIP logo

testest's Introduction

tools.testest

"If thou testest me, thou wilt find no wickedness in me" Psalms 17:3

Vocabulary to test Factor code on Codewars

tools.testest is a Factor vocabulary for writing test cases on Codewars, tuned to the Codewars framework by appropriately formatted messages written by nomennescio (https://github.com/nomennescio).

General setup

For code testing a solution solve:

IN: your-kata
: solve ( ... )
... your solution ...

in your test code USE: tools.testest and structure your tests. Tests are run by executing the MAIN: entrypoint, which points to a word with no stack effects. A typical generic test setup looks like:

USING: tools.testest your-kata.preloaded ... vocabs used by tests ... ;
FROM: your-kata => solve ;
IN: your-kata.tests

: run-tests ( -- )
  ... your test code ...
;

MAIN: run-tests

where run-tests calls solve with different test vectors for each test case.

Test cases

Test cases are partitioned into one or more describe#{ sections, which can be nested, where each section has one or more test cases grouped under section it#{. A single test case is run by <{ ... actual results ... -> ... expected results ... }> where both sides can contain words that are called to process inputs and/or outputs. The test case checks if the implicit sequence of actual results on the left of -> compares equal (using the = word) to the sequence of expected results on the right. It passes upon success, and fails upon failure. This is reported in the Codewars runner. Note that any <{ .. }> does not affect the stack when it has completed. Both describe#{ and it#{ sections report their execution time (load and compile time before MAIN: can be determined from the environment).

Typical Factor testcode to test a solution solve ( a b c -- d ) looks like:

: your-solve ( a b c -- d )
  ... your reference solution ...
;

:: run-tests ( -- )
  "Specific test cases" describe#{
    "Test case 1" it#{
      <{ 1 2 3 solve -> 6 }>
    }#
    "Test case 2" it#{
      <{ 4 5 6 solve -> 15 }>
    }#
  }#
  "Random test cases" describe#{
    "Single Test Group" it#{
      100 [
        1000 random :> r
        <{ r r 3 * r 5 - solve -> r r 3 * r 5 - your-solve }>
      ] times
    }#
    100 [
      "Testing for " 1000 random :> r r number>string append
      it#{ <{ r 2 4 solve -> r 2 4 reference-solve }> }#
    ] times
  }#
;

Custom pass and fail messages

Default messages are shown when a test passes or fails. These messages can be customised using with-passed, with-failed, and with-passed-failed combinators, with the following stack effects:

: with-passed ( passed quot -- )
: with-failed ( failed quot -- )
: with-passed-failed ( passed failed quot -- )

Both passed and failed are quotations, one of which is called after each test inside quot is executed when it passes or fails. The signatures of passed and failed are:

: passed ( -- )
: failed ( error -- )

The argument to failed is an assert-sequence error with slots got for a sequence of actual results and expected for a sequence of expected results, the last element is the top of the stack. Both the passed and failed quotations can write messages to the output stream. Newlines are converted to platform specific line separators.

Custom messages can be nested, and are restored outside the scope of the quotation passed to the with-passed, with-failed, and with-passed-failed combinators.

Custom pass and fail messages example

: run-tests ( -- )
  ...
  [ "Just passed" ] [
    [ [ "Expected: " write expected>> . ] [ nl "Actual: " write got>> . ] bi ] [
      <{ 1 4 add -> 5 }>
      <{ 2 3 add -> 5 }>
    ] with-failed
  ] with-passed
  ...
;

Handling of errors

Factor can throw errors, and as errors are first-class objects, the testest library supports testing of thrown errors in an intuitive way. If test code on the right side of the arrow -> throws an error, the left side is expected to throw that same error, if it does, the test passes, if it doesn't the test fails. On the other hand, if the code on the right does not throw an error, any error thrown by the left side is considered as an unexpected error and reported as such. To help analysing thrown errors, all ERROR:s are printed in a special format by the default failure handler, and thrown errors are marked. All errors inside <{ .. }> are captured and not rethrown.

Handling of errors example

ERROR: custom-error error-message integer-argument ;

: run-tests ( -- )

"System and custom errors tests" describe#{
  "Unexpected divide by 0 error in user code" it#{
     <{ 1 0 / -> 1 }>
  }#
  "Unexpected custom error in user code" it#{
     <{ "thrown custom error" 1 custom-error -> 1 }>
  }#
  "Expected custom error" it#{
     <{ "thrown custom error" 1 custom-error -> "thrown custom error" 1 custom-error }>
  }#
  "Missing expected custom error" it#{
     <{ 1 -> "thrown custom error" 1 custom-error }>
  }#
}#

Inexact value comparisons with margins

In some situations solutions compute an inexact result. The math.margins vocabulary can be used to convert a real result into a margin, which can then be compared against a predefined expected value with acceptable margins defined using the implicitly used = by each test case. Three types of margins are supported: margin, abs-margin, and rel-margin, representing general margins, margins with absolute error bound, and margins with a relative error bound respectively, each with a different visual representation, but giving the same outcome from a comparison point of view. These can be constructed with

: <margin> ( from central to -- margin )
: [a-e,a+e] ( a epsilon -- margin )
: [a-%,a+%] ( a percent -- margin )

The latter two have aliases ± for [a-e,a+e] and ±% for [a-%,a+%].

Reals can be converted in margins for comparison with >margin or its alias . Comparing using = with a constructed margin will compare against the margin's boundaries, if the real falls within the boundaries true is returned, else false.

Margin comparisons are not true equivalence relations, but are tolerance relations, as they are reflexive and symmetric, but not necessarily transitive.

Inexact value comparison with margins example

: run-tests ( -- )
  "Example test" describe#{
    "Absolute margin" it#{ <{ 20 sin >± -> 0 1 ± }> }#
    "Relative margin" it#{ <{ calculate-pi >± -> pi 1 ±% }> }#
  }#
;

The first example calculates a sine, converts to a margin, then compares against 0±1, i.e. all values within the range [-1,1] will pass the test. The second examples calculates pi to a certain precision, converts it to margin, then compares against builtin pi±1%, i.e. all values within the range [pi-pi*1/100, pi+pi*1/100] will pass the test.

Utility words

Some users requested features to be added to the library, which although useful, might not be common enough to warrant their inclusion yet.

I list these usecases here, with utility words which implement the feature

Minimum margin for relative margins

When using relative margins, the closer the value gets to zero, the smaller the effective margin gets, until it is zero at value zero. The %e word calculates a relative margin such that it will never get smaller than epsilon

: %e ( value percent epsilon -- value max(rel,abs) ) [ over * 100 / ] dip max ;

Example usage

pi 1 1e-10 %e ±

creates a margin of pi within 1%, or absolute margin 1e-10, whatever is larger

testest's People

Contributors

kacarott avatar kazk avatar nomennescio avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

testest's Issues

Support inexact unit testing

Hi there! I have run into an issue where I need to test floating point values for equality, given an epsilon of, say, 1e-15. Is there any way the test vocab could be expanded, maybe with ~> syntax, to use (unit-test~) from tools.test.private?

Thanks for your consideration!

Keep timestamps off the stack

(I am not sure why this this wasn't raised as an issue, and have just remembered it now, so moving it to a more appropriate place).
Currently, both it#{ and describe#{ leave a timestamp on the stack, used for reporting the runtime of the users code. This is essentially an implementation detail, but the issue is that it can interfere with user code. Consider this:

"tests" describe#{
   <some-large-data-structure>
   "should work in context A" it#{
     dup run-test-a
   }#
  
   "should work in context B" it#{
     dup run-test-b
   }#
  drop
}#

This seems fairly regular code, however without the author realising it, it will be calling both run-test words with some arbitrary number, instead of the expected data structure!

There is really no reason for the timestamps to be left on the stack like this, and it is simple to fix. Simply scan the relevant block, and run the resulting quotation inside dip such that the timestamp is moved to the retain stack. See this for a simple proof of concept.

LHS is not expected to throw, when RHS throws

According to the docs:

If test code on the right side of the arrow -> throws an error, the left side is expected to throw that same error, if it does, the test passes, if it doesn't the test fails.

However this is not the case. When the RHS throws an error, the actual behaviour is that the resulting object is compared against any resulting values from the LHS, regardless of whether the LHS threw or not.

For example, the following word would pass, despite not throwing.

: should-throw ( -- msg ) "Error!" ;

<{ should-throw -> "Error!" throw }>

I have created a Kumite providing more in depth examples of where the behaviour is unexpected.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.