Skip to content
Enhanced `spec` testing library for [Crystal](http://crystal-lang.org/).
Crystal Other
  1. Crystal 99.8%
  2. Other 0.2%
Branch: master
Clone or download

Latest commit

Fetching latest commit…
Cannot retrieve the latest commit at this time.

Files

Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
scripts
spec
src
unit_spec
.gitignore
.travis.yml
LICENSE
README.md
shard.yml
unit

README.md

spec2 Build Status

Enhanced spec testing library for Crystal.

Example

Spec2.describe Greeting do
  subject { Greeting.new }

  describe "#greet" do
    context "when name is world" do
      let(name) { "world" }

      it "greets the world" do
        expect(subject.greet(name)).to eq("Hello, world")
      end
    end
  end
end

Installation

Add it to shard.yml

dependencies:
  spec2:
    github: waterlink/spec2.cr
    version: ~> 0.9

Goals

  • No global scope pollution
  • No Object pollution
  • Ability to run examples in random order
  • Ability to specify before and after blocks for example group
  • Ability to define let, let!, subject and subject! for example group

Roadmap

1.0

  • Configuration through CLI interface.
  • Filters.
  • Shared examples and example groups.

Usage

require "spec2"

Top-level describe

Spec2.describe MySuperLibrary do
  describe Greeting do
    # .. example groups and examples here ..
  end
end

If you have test suite written for Spec and you don't want to prefix each top-level describe with Spec2., you can just include Spec::GlobalDSL globally:

include Spec2::GlobalDSL

# and then:
describe Greeting do
  # ...
end

Writing examples

Spec2.describe "some tests" do
  it "is a test name here" do
    # .. this is the example here ..
  end

  pending "is a pending test here" do
    # .. this example will not be executed ..
  end
end

Expect syntax

expect(greeting.for("john")).to eq("hello, john")

If you have big codebase that runs on Spec, you can use this to enable #should and #should_not on Object:

Spec2.enable_should_on_object

List of builtin matchers

  • eq("hello, world") - asserts actual is equal to expected
  • raise_error(ErrorClass [, message_matcher]) - checks if block raises expected error
  • be(42) - asserts actual is the same as expected
  • match(/hello .+/) - asserts actual is matching provided regexp
  • be_true - asserts actual is equal true
  • be_false - asserts actual is equal false
  • be_truthy - asserts actual is not nil or false
  • be_falsey - asserts actual is nil or false
  • be_nil - asserts actual is equal nil
  • be_close(42, 0.01) - asserts actual is in delta-proximity of expected
  • expect(42).to_be < 45 - asserts arbitrary method call on actual to be truthy
  • be_a(String) - asserts actual to be of expected type (uses is_a?)

Random order

Spec2.random_order

# this is what happens under the hood
Spec2.configure_order(Spec2::Orders::Random)

To configure your own custom order you can use:

Spec2.configure_order(MyOrder)

Class MyOrder should implement Order protocol and Order::Factory class protocol (see it here). See also a random order implementation.

No color mode

Spec2.nocolor

# this is what happens under the hood
Spec2.configure_output(Spec2::Outputs::Nocolor)

To configure your own custom output you can use:

Spec2.configure_output(MyOutput)

Class MyOutput should implement Output protocol and Output::Factory class protocol (see it here). See also a default colorful output implementation.

Documentation reporter

Spec2.doc

# this is what happens under the hood
Spec2.configure_reporter(Spec2::Reporters::Doc)

To configure your own custom reporter you can use:

Spec2.configure_reporter(MyReporter)

Class MyReporter should implement Reporter protocol and Reporter::Factory class protocol (see it here). See also a default reporter implementation.

If you are creating a custom reporter, you might want to use ElapsedTime class to report elapsed time for the test suite. Example usage:

output.puts "Finished in #{::Spec2::ElapsedTime.new.to_s}"

Configuring custom Runner

Spec2.configure_runner(MyRunner)

Class MyRunner should implement Runner protocol and Runner::Factory class protocol (see it here). See also a default runner implementation.

before

before - register a hook that is run before any example in current and all nested contexts.

before { .. do some stuff .. }

after

after - register a hook that is run after any successful example in current and all nested contexts.

after { .. do some stuff .. }

let

let(name) { value } - register a binding of certain value to name. Lazy: provided block will only be evaluated when needed in example and only once per example.

let(answer) { 42 }

it "is correct answer" do
  expect(answer).to eq(42)
end

let!

let(name) { value } - register a binding of certain value to name. It is not lazy: provided block will be evaluated before each example exactly once.

let!(answer) { 42 }

it "is correct answer" do
  expect(answer).to eq(42)
end

described_class

For describe ... blocks, that describe a class, there is a shortcut to reference that class:

describe Example do
  it "can be created" do
    expect(described_class.new.greet).to eq("hello world")
    # instead of `Example.new.greet`.
  end
end

subject

subject { value } - register a subject of your test with provided value. Lazy.

subject { Stuff.new }

it "works" do
  expect(subject.answer).to eq(42)
end

subject(name) { value } - registers a named subject of your test with provided value with provided name. Lazy.

subject(stuff) { Stuff.new }

it "works" do
  expect(stuff.answer).to eq(42)
end

subject!

subject! { value } - register a subject of your test with provided value. It is not lazy.

subject! { Stuff.new }

it "works" do
  expect(subject.answer).to eq(42)
end

subject!(name) { value } - registers a named subject of your test with provided value with provided name. It is not lazy.

subject!(stuff) { Stuff.new }

it "works" do
  expect(stuff.answer).to eq(42)
end

delayed

Use delayed { ... } to verify expectations after test example and its after hooks finish. Example:

it "does something interesting eventually" do
  delayed { expect(value).to eq(42) }
  # .. do something else, that should eventually lead to value == 42 ..
end

Custom matchers

First, define your matcher implementing this protocol:

class MyMatcher(T, E)
  include Spec2::Matcher

  @actual_inspect : String?

  def initialize(@expected : T, @stuff : E)
  end

  def match(actual)
    @actual_inspect = actual.inspect

    # return true or false here
  end

  def failure_message
    "Expected to be valid #{@stuff.inspect}.
    Expected: #{@expected.inspect}.
    Actual:   #{@actual_inspect}."
  end

  def failure_message_when_negated
    "Expected to be invalid #{@stuff.inspect}.
    Expected: #{@expected.inspect}.
    Actual:   #{@actual_inspect}."
  end

  def description
    "(stuff in #{@expected} #{stuff})"
  end
end

And then, register shortcut helper method to use your matcher.

Spec2.register_matcher(stuff) do |stuff, expected|
  MyMatcher.new(expected, stuff)
end

And use it:

describe "stuff" do
  it "is valid stuff" do
    expect(something).to stuff(some_stuff, "expected stuff")
  end
end

Development

After you forked the repo:

  • run crystal deps to install dependencies
  • run crystal spec and crystal unit to see if tests are green (or just run scripts/test to run them both)
  • apply TDD to implement your feature/fix/etc

Contributing

  1. Fork it ( https://github.com/waterlink/spec2.cr/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors

  • waterlink Oleksii Fedorov - creator, maintainer
You can’t perform that action at this time.