|
| 1 | +--- |
| 2 | +title: Test your JavaScript with QuickCheck |
| 3 | +author: Phil Freeman |
| 4 | +published: 2015-07-16 |
| 5 | +--- |
| 6 | + |
| 7 | +[QuickCheck](http://en.wikipedia.org/wiki/QuickCheck) is a _property-based_ testing library which was originally written in Haskell, but which has been ported to [a](https://github.com/mcandre/node-quickcheck) [number](https://github.com/mcandre/qc) [of](https://github.com/mcandre/cl-quickcheck) [other](https://pypi.python.org/pypi/pytest-quickcheck/) [languages](https://github.com/hayeah/rantly). |
| 8 | + |
| 9 | +QuickCheck works by generating random data with which to test your properties. This allows us to gain confidence that the properties hold, as more and more randomly-generated tests are run. |
| 10 | + |
| 11 | +[`purescript-quickcheck`](http://github.com/purescript/purescript-quickcheck) is a port of QuickCheck to PureScript, which preserves the syntax and types of the original Haskell code. |
| 12 | + |
| 13 | +`purescript-quickcheck` can be used to test code which is written in PureScript, but can be used to test Javascript functions as well. In this post, I'll demonstrate each of these use cases. |
| 14 | + |
| 15 | +#### Testing PureScript Functions |
| 16 | + |
| 17 | +QuickCheck defines the `quickCheck` function, which will print test results to the console, or fail with an exception in the event of a test failure. |
| 18 | + |
| 19 | +Create a new project using Pulp, install `purescript-quickcheck`, and open PSCi: |
| 20 | + |
| 21 | +```text |
| 22 | +pulp init |
| 23 | +bower install purescript-quickcheck |
| 24 | +pulp repl |
| 25 | +``` |
| 26 | + |
| 27 | +Start by importing the QuickCheck library: |
| 28 | + |
| 29 | +``` |
| 30 | +> import Prelude |
| 31 | +> import Test.QuickCheck |
| 32 | +``` |
| 33 | + |
| 34 | +The `quickCheck` function takes one argument: the property you would like to test. Let's try a very simple property: |
| 35 | + |
| 36 | +``` |
| 37 | +> quickCheck \n -> n + 1 == 1 + n |
| 38 | +``` |
| 39 | + |
| 40 | +If everything worked, you should see the following result: |
| 41 | + |
| 42 | +``` |
| 43 | +100/100 test(s) passed. |
| 44 | +unit |
| 45 | +``` |
| 46 | + |
| 47 | +This indicates 100 successful random test runs. |
| 48 | + |
| 49 | +#### Error Messages |
| 50 | + |
| 51 | +Let's see what happens when we try testing a broken property: |
| 52 | + |
| 53 | +``` |
| 54 | +> quickCheck \n -> n + 1 == n |
| 55 | +``` |
| 56 | + |
| 57 | +You should see an exception printed to the console: |
| 58 | + |
| 59 | +``` |
| 60 | +Error: Test 1 failed: |
| 61 | +Failed: Test returned false |
| 62 | +``` |
| 63 | + |
| 64 | +That's not a very helpful error, so let's improve it: |
| 65 | + |
| 66 | +``` |
| 67 | +> quickCheck \n -> n + 1 == n <?> "Test failed for input " <> show n |
| 68 | +``` |
| 69 | + |
| 70 | +This time you should see the following failure message: |
| 71 | + |
| 72 | +``` |
| 73 | +Error: Test 1 failed: |
| 74 | +Test failed for input -654791 |
| 75 | +``` |
| 76 | + |
| 77 | +Alternatively, we could use the `===` operator, which provides a better error message: |
| 78 | + |
| 79 | +``` |
| 80 | +> quickCheck \n -> n + 1 === n |
| 81 | +Error: Test 1 failed: |
| 82 | +-663820 /= -663821 |
| 83 | +``` |
| 84 | + |
| 85 | +#### Example 1 - GCD Function |
| 86 | + |
| 87 | +Let's write an implementation of the _greatest common divisor_ function in PSCi. |
| 88 | + |
| 89 | +This function is easiest to enter in the multi-line input mode, so switch to |
| 90 | +that by entering `:paste` at the prompt: |
| 91 | + |
| 92 | +``` |
| 93 | +> :paste |
| 94 | +… gcd 0 n = n |
| 95 | +… gcd n 0 = n |
| 96 | +… gcd n m | n < 0 = gcd (-n) m |
| 97 | +… gcd n m | m < 0 = gcd n (-m) |
| 98 | +… gcd n m | n > m = gcd (n - m) m |
| 99 | +… gcd n m = gcd n (m - n) |
| 100 | +… |
| 101 | +``` |
| 102 | + |
| 103 | +After typing that in, press Control-D to finish the input. |
| 104 | + |
| 105 | +Now let's assert some basic properties that we expect to hold of the `gcd` function. |
| 106 | + |
| 107 | +``` |
| 108 | +> quickCheck \n -> gcd n 1 === 1 |
| 109 | +``` |
| 110 | + |
| 111 | +This test should pass, but might take a while, because the standard random generator for integers which comes bundled with `purescript-quickcheck` generates integers in the range -1000000 to 1000000. |
| 112 | + |
| 113 | +We can modify our test to only consider small integers: |
| 114 | + |
| 115 | +``` |
| 116 | +> quickCheck \n -> gcd (n / 1000) 1 === 1 |
| 117 | +``` |
| 118 | + |
| 119 | +This time, the test should complete quickly. However, we've coupled the generation of our data (`/ 1000`) with the property we're testing, which is against the spirit of QuickCheck. A better approach is to define a `newtype` which can be used to generate small integers. |
| 120 | + |
| 121 | +Create a new file `src/SmallInt.purs` and paste the following code: |
| 122 | + |
| 123 | +``` |
| 124 | +module SmallInt where |
| 125 | +
|
| 126 | +import Prelude |
| 127 | +
|
| 128 | +import Test.QuickCheck |
| 129 | +import Test.QuickCheck.Arbitrary |
| 130 | +
|
| 131 | +data SmallInt = SmallInt Int |
| 132 | +
|
| 133 | +runInt :: SmallInt -> Int |
| 134 | +runInt (SmallInt i) = i |
| 135 | +
|
| 136 | +instance arbSmallInt :: Arbitrary SmallInt where |
| 137 | + arbitrary = map (SmallInt <<< (_ / 1000)) arbitrary |
| 138 | +``` |
| 139 | + |
| 140 | +Back in PSCi, we can now test properties without having to explicitly define how to generate our random data: |
| 141 | + |
| 142 | +``` |
| 143 | +> quickCheck \(SmallInt n) (SmallInt m) -> gcd n m == gcd m n |
| 144 | +``` |
| 145 | + |
| 146 | +The idea is that the particular scheme that is chosen to generate data should be indicated by the types of our function arguments, so `newtype`s can be quite useful when defining multiple data generation schemes for a single type. |
| 147 | + |
| 148 | +#### Example 2 - Testing Higher Order Functions |
| 149 | + |
| 150 | +QuickCheck can also be used to test higher-order functions, by randomly generating functions. |
| 151 | + |
| 152 | +Let's test that the `map` function on arrays satisfies the functor laws. |
| 153 | + |
| 154 | +For these two tests, I will write the test function using a let binding to avoid having to write type signatures in properties. |
| 155 | + |
| 156 | +The first functor law says that if you map a function which does not modify its argument (the identity function) over a structure, then the structure should not be modified either. |
| 157 | + |
| 158 | +``` |
| 159 | +> import Data.Array |
| 160 | +> :paste |
| 161 | +… firstFunctorLaw :: Array Int -> Boolean |
| 162 | +… firstFunctorLaw arr = map id arr == arr |
| 163 | +… |
| 164 | +> quickCheck firstFunctorLaw |
| 165 | +100/100 test(s) passed. |
| 166 | +unit |
| 167 | +``` |
| 168 | + |
| 169 | +The second functor law says that mapping two functions over a structure one-by-one is equivalent to mapping their composition over the structure: |
| 170 | + |
| 171 | +``` |
| 172 | +> :paste |
| 173 | +… secondFunctorLaw :: (Int -> Int) -> (Int -> Int) -> Array Int -> Boolean |
| 174 | +… secondFunctorLaw f g arr = map f (map g arr) == map (f <<< g) arr |
| 175 | +… |
| 176 | +> quickCheck secondFunctorLaw |
| 177 | +100/100 test(s) passed. |
| 178 | +unit |
| 179 | +``` |
| 180 | + |
| 181 | +#### Testing Javascript Functions |
| 182 | + |
| 183 | +Now let's try an example of testing a function written in Javascript. |
| 184 | + |
| 185 | +[This file](https://gist.github.com/paf31/3aedd6c3e3ac5c8a78e7) contains a set of FFI bindings for some of the functions defined by the [UnderscoreJS](http://underscorejs.org/) library. It is a nice example of a set of pure functions written in Javascript which we can test with QuickCheck. |
| 186 | + |
| 187 | +Copy the contents of that file into `src/UnderscoreFFI.purs`, and reload PSCi with that module loaded: |
| 188 | + |
| 189 | +``` |
| 190 | +> import UnderscoreFFI |
| 191 | +``` |
| 192 | + |
| 193 | +The `UnderscoreFFI` module defines a wrapper for the `sortBy` function. Let's test that the function is idempotent: |
| 194 | + |
| 195 | +``` |
| 196 | +> :paste |
| 197 | +… sortIsIdempotent :: Array Int -> Boolean |
| 198 | +… sortIsIdempotent arr = sortBy id (sortBy id arr) == sortBy id arr |
| 199 | +… |
| 200 | +> quickCheck sortIsIdempotent |
| 201 | +100/100 test(s) passed. |
| 202 | +unit |
| 203 | +``` |
| 204 | + |
| 205 | +In fact, we don't need to sort by the identity function. Since QuickCheck supports higher-order functions, we can test with a randomly-generated sorting function: |
| 206 | + |
| 207 | +``` |
| 208 | +> :paste |
| 209 | +… sortIsIdempotent' :: (Int -> Int) -> Array Int -> Boolean |
| 210 | +… sortIsIdempotent' f arr = sortBy f (sortBy f arr) == sortBy f arr |
| 211 | +… |
| 212 | +> quickCheck sortIsIdempotent |
| 213 | +100/100 test(s) passed. |
| 214 | +unit |
| 215 | +``` |
| 216 | + |
| 217 | +Have a look through the `UnderscoreFFI` module, and see what other properties you can define. |
| 218 | + |
| 219 | +#### Conclusion |
| 220 | + |
| 221 | +Hopefully I've shown that QuickCheck can be a useful tool, whether you write your code in PureScript or not. Its strength is in its _type-directed_ approach to data generation, which allows you to say _what_ you want to test directly, rather than _how_ to generate test data. |
0 commit comments