test.check

repository·master·Indexed 22 days ago

https://github.com/clojure/test.check

A property-based testing library for Clojure inspired by QuickCheck. It allows developers to define logical assertions (properties) that are tested against a wide range of automatically generated inputs rather than hardcoded cases. The library provides a comprehensive set of generators for primitive types, collections, and combinators to build complex data generators.

Tokens
8.9K
Snippets
30
Records
44
Agent score
78%

What's inside test.check

  1. What is test.check?

    master
    test.check is a property-based testing tool for Clojure inspired by QuickCheck. Instead of writing unit tests that enumerate specific inputs and expected outputs, you define properties (logical assertions) that should hold true for all possible inputs generated by the library. This allows for concise and powerful testing by automatically exploring a wide range of input space.
  2. Introduction to property-based testing with test.check

    master

    Unlike traditional unit testing that uses specific test cases, test.check uses property-based testing. You define properties (universal quantifications) that should hold true for all possible inputs.

    When a property fails, test.check automatically performs shrinking: it attempts to find the smallest, simplest input that still causes the failure, making debugging much easier.

    (require '[clojure.test.check :as tc]
             '[clojure.test.check.generators :as gen]
             '[clojure.test.check.properties :as prop])
    
    ;; Define a property: for all vectors v, sorting v should preserve count and be ordered
    (def property
      (prop/for-all [v (gen/vector gen/small-integer)]
        (let [s (sort v)]
          (and (= (count v) (count s))
               (or (empty? s)
                   (apply <= s))))))
    
    ;; Run the test with 100 iterations
    (tc/quick-check 100 property)
  3. Difference between gen/fmap and gen/bind

    master

    Both gen/fmap and gen/bind are used to transform generators, but they serve different purposes based on what the transformation function returns:

    Featuregen/fmapgen/bind
    Function Return TypeReturns a valueReturns a generator
    Use CaseTransforming an existing value (e.g., sorting a vector, multiplying a number)Creating a new generator based on a previous value (e.g., picking an element from a generated vector)
    Argument Order(gen/fmap f generator)(gen/bind generator f)
  4. Understand how the `size` parameter affects data generation

    master

    In test.check, the size parameter controls the complexity of generated data. Generators use this value to determine the scale of the output (e.g., the magnitude of an integer or the length of a collection).

    Key behaviors:

    • Integer Generators: For many integer generators, the range is roughly proportional to size. For example, gen/nat produces numbers roughly proportional to size, while gen/large-integer produces much larger values regardless of size.
    • Collection Generators: A collection's length and the complexity of its elements typically grow with size. If you use a fixed-size generator like (gen/vector gen/nat 3), the collection length remains constant while the elements grow.
    • Generator Independence: Some generators, like gen/uuid, ignore the size parameter entirely.

    To experiment with how a generator responds to different sizes, use gen/generate with an explicit size argument.

    (defn sizing-sample
      [g]
      (into {}
       (for [size [0 5 25 200]]
         [size
          (repeatedly 5 #(gen/generate g size))])))
    
    ;; Example: observing growth in gen/nat
    (sizing-sample gen/nat)
    ;; => {0   (0 0 0 0 0),
    ;;     5   (4 1 3 3 5),
    ;;     25  (12 8 24 25 22),
    ;;     200 (63 143 31 199 7)}
  5. How `size` changes during a `quick-check` run

    master

    When running clojure.test.check/quick-check, the library automatically increments the size parameter for each trial to gradually increase test complexity.

    It follows the pattern (cycle (range 200)). This means:

    • Trial 1 uses size=0.
    • Trial 2 uses size=1.
    • ...
    • Trial 200 uses size=199.
    • Trial 201 resets to size=0.

    This progression allows test.check to catch simple bugs early with small inputs and edge cases before attempting more complex, larger inputs.

    Warning: If you run fewer than 200 trials, you may not experience the full range of sizes. Specifically, tests with fewer than ~10 trials receive very poor coverage because they only see the smallest possible sizes.

  6. Handling NaN in Double Generators

    master

    In test.check, gen/double and gen/double* generate NaN by default. This is intended to test edge cases, but because NaN != NaN, it can cause issues in data structures that rely on equality.

    To avoid NaN values in your tests, use gen/double* instead of gen/double. This allows you to opt-out of NaN generation when appropriate for your specific property.

    ;; Use gen/double* to avoid NaN if your code requires equality checks
    (gen/double*)
  7. Understand shrinking in test.check

    master

    When a property fails, test.check performs a process called shrinking. It attempts to find the 'smallest' or simplest input that still causes the test to fail. This is critical for debugging complex inputs.

    In the test results:

    • :fail contains the original large input that caused the failure.
    • :shrunk is a map containing the results of the shrinking process.
    • :shrunk :smallest contains the minimal input that triggers the failure.

    For example, if a property fails because a vector contains the number 42, test.check might initially fail on a large vector like [-35 -9 ... 42 ... 17], but will shrink the input down to just [42].

  8. Create compound generators

    master

    You can create complex generators by passing existing generators as arguments to compound generators. This allows for the creation of heterogeneous collections or nested structures.

    Common compound generators include:

    • gen/vector: Generates a vector of values from a provided generator.
    • gen/list: Generates a list of values.
    • gen/map: Generates a map with keys and values from provided generators.
    • gen/tuple: Generates a vector containing one value from each of the provided generators.
    (require '[clojure.test.check.generators :as gen])
    
    ;; Vector of natural numbers
    (gen/sample (gen/vector gen/nat))
    
    ;; Map with keyword keys and boolean values
    (gen/sample (gen/map gen/keyword gen/boolean) 5)
    
    ;; Heterogeneous tuple: (nat, boolean, ratio)
    (gen/sample (gen/tuple gen/nat gen/boolean gen/ratio))
  9. Generating Sequences of Stateful Events

    master

    When testing systems where future events depend on the current state (e.g., a sequence of create, update, and delete operations), using gen/bind can often thwart the shrinking process.

    A more robust pattern for generating these sequences is to use a generator that takes an initial state and a reduction function. This allows the generator to produce a sequence of events that are valid according to the state produced by the preceding events.

    ;; Conceptual pattern for a stateful event generator
    (defn gen-events
      """Given an init-state and a reduce function for determining the
      current state from a sequence of events, together with a function that takes a state and returns a generator of a new event, returns a generator of sequences of events."""
      [reduce-func init-state state->event-gen]
      ...)
  10. Avoid memory exhaustion with nested collection generators

    master

    By default, test.check collection generators select a size proportional to the size parameter. When nesting generators (e.g., a vector of vectors of vectors), this can lead to exponential growth in the total number of elements, potentially exhausting available memory.

    Mitigation: Use strategic resizing (via gen/scale or gen/resize) on the inner generators to ensure that as the outer collection grows, the inner collections grow at a much slower rate (e.g., logarithmically).

    ;; Example of scaling down inner collection size to prevent exponential growth
    (def gen-small-vectors-of-large-numbers
      (gen/scale #(max 0 (Math/log %))
                 (gen/vector (gen/scale #(* % 100) gen/large-integer))))
  11. Run ClojureScript tests in a web browser

    master

    To execute the ClojureScript tests within a web browser environment:

    1. First, run lein cljsbuild once to prepare the build.
    2. Open either test-runners/run_tests_dev.html or test-runners/run_tests_adv.html in your browser.
    3. Monitor the JavaScript console for the test output.