PureScript Spec - The Guide

Contents

Introduction

PureScript Spec is a simple testing framework for Purescript, inspired by hspec. Use PureScript Spec to write synchronous and asynchronous tests using a simple DSL, combine with other testing tools, and generate test output in various formats.

Installation

Given that you already have a PureScript project setup, the first thing to do is installing purescript-spec as a development dependency.

bower install --save-dev purescript-spec

Writing Specs

The basic building block of spec writing is it, which creates a spec with a spec body. Spec bodies have the type Aff r Unit, which is similar to the Eff type, but with the addition of asynchronicity. When specs are run, they are considered successful, or passing, if the Aff computation does not result in an error. For more information, see purescript-aff.

In the following example we use pure unit as a body, which does nothing. It will not throw an error, and the spec will always pass.

it "does nothing" $ pure unit

A more interesting test would assert something. Let’s check that addition works!

it "adds 1 and 1" do
  1 + 1 `shouldEqual` 2

The shouldEqual function, here used as an infix operator, takes two values and checks if they are equal. If not, it throws an error in the Aff monad, causing the spec to fail.

Specs can also be pending, which means that they are not testing anything yet - they are like placeholders. We use pending to write a pending spec.

pending "calculates the answer to Life, the Universe and Everything"

Pending specs can also contain a spec body, just like with it. The difference is that the body will be ignored. Pending spec bodies are used to give a hint what the spec should assert in the future. Use pending' (note the ' at the end) to create a pending spec with a body.

pending' "calculates the answer to Life, the Universe and Everything" do
  answerTo theUltimateQuestion `shouldBe` 42

To group multiple specs in a logically related group of specs, we use describe. This creates a new spec which represents the named group.

describe "MyModule" do
  it "..." do
    ...
  it "..." do
    ...
  it "..." do
    ...

Spec groups can be nested in multiple levels, creating a hierarchy of named groups.

describe "MyModule" $
  describe "SubModule" $
    describe "Database" do
      it "..." do
        ...
      it "..." do
        ...
      it "..." do
        ...

Full Example

Let’s look at an example of a complete spec program, with the needed imports and a proper main function. The specs shown in the header image looks like this:

module Main where

import Prelude
import Control.Monad.Aff (later')
import Control.Monad.Eff (Eff)
import Test.Spec (pending, describe, it)
import Test.Spec.Assertions (shouldEqual)
import Test.Spec.Reporter.Console (consoleReporter)
import Test.Spec.Runner (RunnerEffects, run)

main :: Eff (RunnerEffects ()) Unit
main = run [consoleReporter] do
  describe "purescript-spec" do
    describe "Attributes" do
      it "awesome" do
        let isAwesome = true
        isAwesome `shouldEqual` true
      pending "feature complete"
    describe "Features" do
      it "runs in NodeJS" $ pure unit
      it "runs in the browser" $ pure unit
      it "supports streaming reporters" $ pure unit
      it "supports async specs" do
        res <- later' 100 $ pure "Alligator"
        res `shouldEqual` "Alligator"
      it "is PureScript 0.10.x compatible" $ pure unit

RunnerEffects is a convenience type alias that includes the effect rows used by the Node runner, meaning you do not have to type them all out if you want to have a type annotation for your main function. You can pass any extra effect rows used in your own tests:

main :: Eff (RunnerEffects (random :: RANDOM, buffer :: BUFFER)) Unit

Combining Specs

You can split specs into multiple files and combine them using regular monadic bind, e.g. with do expressions.

baseSpecs = do
  mathSpec
  stringsSpec
  arraySpec
  ...

This is often used to combine all specs into a single spec that can be passed to the test runner, if not using purescript-spec-discovery.

Running A Subset of the Specs

Sometimes you do not wish to run all specs. It might be that you are working on a certain feature, and only want to see the results for the relevant tests. It can also be that some spec takes a lot of time, and you wish to exclude it temporarily. By using itOnly instead of the regular it, the test runner includes only that spec.

describe "My API" do
  itOnly "does feature X" ... -- only this spec will run
  it "does things that takes a lot of time"

Similar to itOnly, describeOnly makes the runner include only that group.

describe "Module" do
  describeOnly "Sub Module A" -- only this group will run
    it "does feature X" ...
  describe "Sub Module B"
    it "does feature Y" ...

QuickCheck

You can use QuickCheck together with the purescript-spec-quickcheck adapter to get nice output formatting for QuickCheck tests.

Running

When you have a spec, you need a runner to actually run it and get the results. PureScript Spec comes with a NodeJS runner, run, which takes an array of reporters and a spec to run. What you get back is a test-running program of type Eff r (). The effect rows in r depend on what you do in your specs and what reporters you are using. The program can be run using Pulp.

pulp test

If you’re not using pulp, you can compile the test program using psc. The following command compiles all PureScript modules in test and src.

psc -o output/tests 'test/**/*.purs' 'src/**/*.purs'

After that has finished, you can run the test program using NodeJS.

NODE_PATH=output/tests node -e "require('Test.Main').main();"

NOTE: A test program using Test.Spec.Runner.run cannot be browserified and run in the browser, it requires NodeJS. To run your tests in a browser, see Browser Testing below.

Reporters

Reporters can be passed to the runner, e.g. run [reporter1, ..., reporterN] spec. Currently there are these reporters available:

Passing Runner Configuration

In addition to the regular run function, there is also run', which takes a Config record.

main = run' testConfig [consoleReporter] mySpec
  where
    testConfig = { slow: 5000, timeout: Just 10000 }

The Test.Spec.Runner module provides a defaultConfig value which you can use to override only specific values.

main = run' testConfig [consoleReporter] mySpec
  where
    testConfig = defaultConfig { slow = 100 }

Automatically Discovering Specs

If you are running your specs in an NodeJS environment, e.g. with pulp test, you can automatically scan for spec modules using purescript-spec-discovery. Then your main function can be as simple as:

main = discover "My\\.Package\\..*Spec" >>= run [consoleReporter]

All modules matching the pattern, that has a spec :: Spec r () definition will be combined into a single large spec by discover.

Browser Testing

You can run tests in a browser environment, instead of NodeJS, using mocha or karma. For more information, see purescript-spec-mocha.

Next Steps

purescript-spec on Pursuit features version information and API documentation. For updates on the project, follow @owickstrom at Twitter.

The source code is available on GitHub.

Contribute

If you have any issues or possible improvements please file them as GitHub Issues. Pull requests are encouraged.