img043

Interactive development with clojure.spec

Simple Machines 9th November, 2016

Lorem ipsum dolor sit a met, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation.

Clojure.spec provides seamless integration with clojure.test.check’s generators. Write a spec, get a functioning generator, and you can use that generator in a REPL as you’re developing, or in a generative test.

If you like, you can follow along by evaluating the forms (in order of appearance – some redef vars that are defd earlier in the namespace) in codebreaker.clj.

Image source: Stack Overflow

Image source: Stack Overflow

Problem

We want a function that accepts a secret code and a guess, and returns a score for that guess. Codes are made of 4 to 6 colored pegs, selected from six colors: [r]edr [y]ellow, [g]reen, [c][/c]yan, [b]lack, and [w]hite. The score is based on the number of pegs in the guess that match the secret code. A peg in the guess that matches the color of the peg in the same position in the secret code is considered an exact match, and a peg that matches a peg in a different position in the secret code is considered a loose match.

For example, if the secret code is [:r :y :g :c] and the guess is [:c :y :g :b], the score would be {:codebreaker/exact-matches 2 :codebreaker/loose-matches 1} because :y and :g appear in the same positions and :c appears in a different position.
We want to invoke this fn with two codes and get back a map like the one above, e.g.

Properties and property-based testing

In property-based testing, we make assertions about properties of a function and provide test data generators. The testing tool then generates test data, applies the function to it, and invokes the assertions. Properties are more general than examples in example based tests. For example, rather than writing a test that expresses the example above and asserts that the resulting map looks exactly like the one above, we’d write expressions that express more general properties like:

  • The return value should be a map with the two keys :codebreaker/exact-matches and :codebreaker/loose-matches
  • the values should be natural (i.e. non-negative) integers
  • the sum of the values should be >= 0
  • the sum of the values should be <= the number of pegs in the secret code
  • We’ll express all of these properties using clojure.spec, and we’re also going to describe the arguments to the function using the same tooling. This is one way in clojure.spec departs from other property-based testing tools.

Properties and property-based testing

In property-based testing, we make assertions about properties of a function and provide test data generators. The testing tool then generates test data, applies the function to it, and invokes the assertions. Properties are more general than examples in example based tests. For example, rather than writing a test that expresses the example above and asserts that the resulting map looks exactly like the one above, we’d write expressions that express more general properties like:

  1. The return value should be a map with the two keys :codebreaker/exact-matches and :codebreaker/loose-matches
  2. the values should be natural (i.e. non-negative) integers
  3. the sum of the values should be >= 0
  4. the sum of the values should be <= the number of pegs in the secret code
  5. We’ll express all of these properties using clojure.spec, and we’re also going to describe the arguments to the function using the same tooling. This is one way in clojure.spec departs from other property-based testing tools.

Properties and property-based testing

In property-based testing, we make assertions about properties of a function and provide test data generators. The testing tool then generates test data, applies the function to it, and invokes the assertions. Properties are more general than examples in example based tests. For example, rather than writing a test that expresses the example above and asserts that the resulting map looks exactly like the one above, we’d write expressions that express more general properties like:

You can easily scan these examples visually and validate that they’re all producing the correct result for :codebreaker/exact-matches.

Н4 Body Subheading

We’ll express all of these properties using clojure.spec, and we’re also going to describe the arguments to the function using the same tooling. This is one way in clojure.spec departs from other property-based testing tools.

:args

We have a few more questions to answer, but that’s enough to get started, which we’ll do with a function spec. We’ll start with just the spec for the arguments, and a couple of supporting definitions.

Experience report

The exact match calculation is easy to imagine: we need to compare each peg in the guess to the peg in the same position in the secret:

I’ve been using TDD in some form or another for many years, and when clojure.spec appeared I was curious to see how it would fit into or change my approach. I won’t go as far as to say that this post represents “my approach” as that is still evolving in light of the presence of spec, but there are clear similarities to and differences from TDD in this example.

Like TDD, there is a tight feedback loop: write a spec and exercise it right away, all before any implementation code. The spec itself is not a test, but it is a reusable source for generated sample data that we can use in an interactive REPL session and in a repeatable test.

Like TDD, I did some refactoring as I discovered opportunities to improve the code. Sometimes I used visual inspection of the result of exercising specs and sometimes I used test.check to cast a wider net.

Unlike TDD, I didn’t go through a consistent cycle of watching a test fail and then making it pass, and then refactoring (red/green/refactor). You can use these tools for that cycle, but generative tests are, by design, more coarse than example based tests, so it might be more of a challenge to keep that very granular cycle consistent. Perhaps a subject for another post (perhaps written by you!).

Forms

Error Helpre Text