Testing CSL

CSL definitions can be tested in various ways. The expression language supports both unit tests and randomized property based tests. The contract language can be tested using special scenario tests.

Unit tests

CSL unit tests are merely normal value definitions of type Test and annotated with the test keyword:

val test onePlusOne =
  Test::unitTest "One plus one" \() -> Test::assertEqualEpsilon 2.0 (1.0 + 1.0)

The unit test above checks that 1 + 1 is 2 (upto a floating point epsilon). Unit tests are constructed using the standard library definition Test::unitTest. Multiple unit tests can be combined with the Test::suite function. Unit tests contain assertions (e.g. Test::assertEqualsEpsilon above). The standard library provides a few standard assertion functions e.g. Test::assertEquals. Additionally, the primitives Test::pass, Test::fail and Test::expected can be used to write more advanced assertions.

Scenario tests

Unit tests work only for CSL values, and not for CSL contracts. Scenario tests allow for testing the behaviour of CSL contracts in response to certain event applications.

type Count : Event { counts: Int }
contract rec entrypoint count = \(acc: Int) ->
  match acc >= 10 with {
    | True -> success
    | False -> <*> e: Count where e.counts >= 0 then count (acc + e.counts)
  }

val totalCounts = \(counts: List Event) ->
  foldl
    (\acc -> \(e: Event) ->
      type count = e of {
        Count -> acc + count.counts;
        _ -> acc
      }
    )
  0
  counts

val ts = Instant::from 0 0

val test countScenario =
  Test::withAgent "Alice" \alice ->
    Test::runScenario "Count test" scenario (count 0) [
      Scenario::event (Count { agent = alice, timestamp = ts, counts = 1 } :> Event),
      Scenario::event (Count { agent = alice, timestamp = ts, counts = 2 } :> Event),
      Scenario::event (Count { agent = alice, timestamp = ts, counts = 4 } :> Event),
      Scenario::event (Count { agent = alice, timestamp = ts, counts = 0 } :> Event),
      Scenario::expectFailure
        "cannot apply negative counts"
        (Scenario::event (Count { agent = alice, timestamp = ts, counts = -1 } :> Event)),
      Scenario::eventsSatisfy "All the counts add up" (\events -> totalCounts events = 7),
      Scenario::event (Count { agent = alice, timestamp = ts, counts = 4 } :> Event),
      Scenario::isFulfilled
    ]

This very contrived example simply tests that the counting contract can accept events until the accumulated sum is equal to or larger than 10. It also tests that negative counts cannot be applied.

Scenario tests support more features, such as composition of tests, rollbacks and external example data. Please see the examples in the the example CSL app repository and check out the full API documentation.

Properties

Properties are CSL functions that describe correctness conditions for CSL code. They serve two purposes:

  1. They help communicate the intended meaning of the code, making it easier to spot mistakes
  2. They can be mechanically checked, helping prevent the introduction of mistakes during development

Often, when writing code, one has a collection of examples in mind. Properties can be used to document these exampels for later use. Properties that represent simple examples are Booleans, with the intention being that they are True when the property holds and False when it does not. In other words, a False property indicates that the code contains a mistake (or that the property itself is incorrect).

val property leapYear2000 = Year::isLeapYear (Year::fromInt 2000)

Properties should be interpreted as statements about the code. In this case, the statement is “the year 2000 is a leap year”. However, in the case of such simpe Boolean properties, and unit test would be better and give a more descriptive failure message.

Properties can also be functions. These properties must return a Boolean value, and their arguments represent “for all” statements. In other words, the property holds if it returns True for all possible argument values.

For example, “converting an Int to a Float and back yields the starting number” can be written as follows:

val property intToFloatToInt = \x -> Float::toInt (Int::toFloat x) = x

Properties may take arbitrary numbers of arguments. For instance, “addition of integers is associative” can be expressed as follows:

val property additionIsAssociative =
    \(x : Int) -> \(y : Int) -> \(z : Int) ->
      x + (y + z) = (x + y) + z
Properties can be used in three ways:
  • Simple Boolean properties are checked to be True.
  • Property functions can be applied to pre-selected lists of arguments.
  • If all arguments are base data types such as Int, or records, unions, or tuples that contain them them, then the CSL tools can randomly generate thousands of test cases, and endure that the property holds for all of them.

Please see the examples repository to see how to do this as part of a standard testing workflow in an application language.