Minispec
Simple, Intuitive, Full-featured Testing Framework
Install | Quick Start | Docs | Contributors | Authors and License
What and Why
Simply, I'm tired of syntax like assert_equal(b, a)
, a.should == b
, expect(a).to eq(b)
etc.
Something closer to a == b
would make more sense.
And I'm tired of learning framework specific techniques.
I want simply to use Ruby's native methods:
assert(a) == b
does(a).include?(b)
is(a).empty?
==
, include?
, empty?
are all Ruby methods called on a
.
What you see around a
is a simple wrapper that passes messages to a
and marks the assertion as passed or failed, depending on returned value.
Install
Add this line to your application's Gemfile:
gem 'minispec'
And then execute:
$ bundle
Or install it yourself as:
$ gem install minispec
then load it using require 'minispec'
Quick Start
Examples borrowed from github.com/rubyspec. Commented are the original assertions.
describe "Array.allocate" do
it "returns an instance of Array" do
ary = Array.allocate
# ary.should be_an_instance_of(Array)
is(ary).instance_of?(Array)
end
it "returns a fully-formed instance of Array" do
ary = Array.allocate
# ary.size.should == 0
assert(ary.size) == 0
ary << 1
# ary.should == [1]
assert(ary) == [1]
end
it "does not accept any arguments" do
# lambda { Array.allocate(1) }.should raise_error(ArgumentError)
does { Array.allocate(1) }.raise?(ArgumentError)
end
end
Docs
- Defining Specs
- Defining Tests
- Shared examples and setups
- Local variables and subject
- Custom error messages
- Hooks
- Assertions
- Helpers
- Mocking
- Running Specs
Defining Specs
There are 2 ways to define Minispec specs: by using Minispec's DSL and by using Ruby classes.
Minispec's DSL has 3 methods that allow to define specs:
describe
context
section
They accepts a single argument(the spec name) and a block containing setups and tests:
describe SomeClass do
# setups and tests
end
When using classes you should include Minispec
:
class SomeSpec
include Minispec
# setups and tests
end
↳ Nested Specs
Minispec allows you to nest specs using the same describe
/context
/section
DSL:
Nested specs inherit everything (except tests) from parent spec, but do not change any parent specs state in any way:
describe :A do
before { @letter = 'A' }
# some tests
describe :B do
before { @letter = 'B' }
# it will run both inherited and own callbacks but wont change A's @letter
end
# @letter is still 'A'
end
Defining Tests
Here are the methods that can be used to define tests:
test
testing
example
should
it
# spec
describe :RequestTest do
# test 1
should 'respond to #user_agent' do
request = Sinatra::Request.new({'HTTP_USER_AGENT' => 'Test'})
does(request).respond_to?(:user_agent)
assert(request.user_agent) == 'Test'
end
# test 2
it 'is secure when the url scheme is https' do
request = Sinatra::Request.new('rack.url_scheme' => 'https')
is(request).secure?
end
# test 3
testing 'it respects X-Forwarded-Proto header for proxied SSL' do
request = Sinatra::Request.new('HTTP_X_FORWARDED_PROTO' => 'https')
is(request).secure?
end
# test 4
it "exposes the preferred type's parameters" do
request = Sinatra::Request.new('HTTP_ACCEPT' => 'image/jpeg; compress=0.2')
assert(request.preferred_type.params) == { 'compress' => '0.2' }
end
# etc.
end
Tests can Not be defined inside another tests. If you need concerns separation use nested specs instead.
↳ Skipping a test
When you need to skip some test simply use skip
method inside test:
should 'work with new Hash syntax' do
skip if RUBY_VERSION < '1.9'
# code here wont be evaluated on Ruby 1.8
end
Any code after skip
method will be just ignored and test reported as skipped.
↳ Mark a test as failed
When you need a custom failure message use fail
method:
# will generate standard failure message
assert(1) == 2
# using custom failure message
1 == 2 || fail('expected 1 to be equal to 2 :(')
Assertions that comes after a failure will be ignored.
If you need all assertions to be evaluated regardless failures use continue_on_failures(true)
at spec level(not inside test).
Shared examples and setups
Often you need to share some setups and tests (a.k.a examples) between various specs.
To define shared setups/examples simply define a module that includes Minispec
.
Later that module can be included into any spec.
module MailboxAssets
include Minispec
before { @mailbox = Mailbox.new }
test '#deliver' do
# ...
end
end
describe :SMTP do
include MailboxAssets
before do
# will run included hooks before ones defined here,
# so @mailbox is available here
@mailbox.transport = :smtp
end
# will set @mailbox's transport to :smtp and run #deliver test
end
describe :sendmail do
include MailboxAssets
before { @mailbox.transport = :sendmail }
# will set @mailbox's transport to :sendmail and run #deliver test
end
↳ Resetting included resources
Minispec will include following resources from base module:
:tests
:helpers
:before
:after
:around
:vars
:continue_on_failures
If you need to reset any of included resource, use reset
method with resources to reset:
module CPUExamples do
# some setups and tests
end
describe :MacBook do
include CPUExamples
reset :before # resets :before hooks
reset :before, :after # resets :before and :after hooks
# etc
end
Local variables and subject
Minispec provide a let
method as a clean way to define local variables. Its block are executed only once per test, when given variable used for first time.
describe Array do
let(:array) { Array.new }
it 'is a Enumerable' do
assert(array).is_a? Enumerable
end
end
subject
allows testing some object without repeatedly typing it. It is automatically set when a spec is defined using Minispec's DSL:
describe Hash do
it 'responds to :[]' do
assert.respond_to? :[]
end
end
In example above the subject are automatically set to Hash
and automatically picked up by assert
.
It can also be written as assert(subject).respond_to? :[]
or even assert(Hash).respond_to? :[]
, in case you prefer more explicit assertions.
Custom error messages
Minispec will do its best to provide detailed failure messages.
However there are cases when you need to use custom messages.
This is easily done by providing your error message as second argument,
using :on_error
key:
assert(pizza, on_error: "Seems not enough olives...").is_tasty
Now if pizza is not tasty enough, Minispec will inform us about the lack of olives rather than just generally complain about poor taste.
Hooks
↳ before
and before_all
before
callback runs before each test:
describe Array do
before { @array = subject.new }
# @array will be different for each test
end
Important: before
callbacks are incremental, meant that all callbacks, inherited and defined, will be called.
First will be called inherited callbacks. Defined ones will run second:
describe :A do
before { @letter = 'A' }
context :a do
before { @letter.downcase! }
# two callbacks will run here:
# 1. @letter = 'A'
# 2. @letter.downcase!
end
# @letter is still 'A' cause child specs does not change parent's state
end
before_all
will run only once, at spec initialization, before any test run:
describe Array do
before_all { @array = subject.new }
# @array will be the same for all tests
end
Important: Unlike before
callbacks, before_all
ones are not incremental, so only the last defined/inherited callback will be called.
↳ after
and after_all
after
will run after each test, regardless was it passed or failed.
Important: Just like before
callbacks, after
ones are incremental, so all callbacks, inherited and defined, will run in appropriate order - first inherited then defined.
after_all
will run only once, after all tests finished. It will run regardless tests status.
Important: Unlike after
callbacks, after_all
ones are not incremental, so only the last defined/inherited callback will be called.
↳ around
and around_all
Allow to run tests inside a predefined wrapper.
The block will receive the test as first argument and should call #run
on it:
describe ActorSystem do
around do |test|
Celluloid::ActorSystem.new.within do
test.run
end
end
# each test will run within own ActorSystem
end
Important: Unlike before
/after
callbacks, around
are not incremental, meant that only the last callback will be called regardless how many callbacks was inherited/defined.
around_all
is similar to around
except it will run all tests inside given block.
It will receive the spec as first argument and should call run
on it:
require 'tmpdir'
describe :FileManager do
around_all do |spec|
# running all tests into a temporary folder
Dir.mktmpdir do
spec.run
end
end
end
↳ Filters
When you need a callback to run only before/after/around specific test(s), pass that tests names as arguments.
Run only before :a
and :b
tests:
before :a, :b do
# ...
end
It is also possible to use :except
option.
Run after all except :x
:
after except: :x do
# ...
end
And to make matchers even more useful, test names can be provided as regular expressions.
Run around tests that match /a/
:
around /a/ do
# ...
end
Run before tests that match /a/
but not before :abc
:
before /a/, except: :abc do
# ...
end
Run before tests that match /a/
but not before ones that match /ab/
:
before /a/, except: /ab/ do
# ...
end
Assertions
Minispec's assertions mechanism is pretty simple: tested objects are wrapped into a proxy that intercepts messages, sending them to tested object and mark assertion as passed or failed based on returned value.
There are plenty of wrappers used in Minispec. assert
and expect
are only few of them:
assert(a) == b
expect(a).include?(b)
assert(a).nil?
# etc.
Though these assertions looks mostly ok, they are not truly semantic.
Let's use some more wrappers:
is(a) == b
does(a).include?(b)
is(a).nil?
# etc.
Here is the list of available wrappers:
assert
affirm
assume
assure
expect
verify
check
prove
would
will
is
is?
are
are?
was
was?
does
does?
did
did?
have
have?
has
has?
↳ Negative assertions
There are two kind of negations in Minispec:
- negative wrappers
- post-wrapper negations
List of negative wrappers:
refute
negate
fail_if
not_expected
assert_not
refute(a) == b
fail_if(a).include?(b)
# etc.
List of post-wrapper negations:
not
has_not
have_not
does_not
did_not
is_not
is_not_a
wont
assert(a).not == b
assert(a).does_not.include?(b)
assert(a).is_not.nil?
# etc.
↳ Semantic sugar
Just like post-wrapper negations, sugar methods are used after a wrapper and are aimed to add some more semantic sense to assertions.
List of semantic sugar methods:
a
is
is_a
are
will
was
does
did
have
has
to
be
been
is(x).a.instance_of?(Y)
assert(a).is.nil?
expect(x).was.called?
assert(x).has.been.locked?
expect(a).to.include?(b)
expect(a).to.be.empty?
expect(x).to.have.children
# etc.
Helpers
In most cases native Ruby methods are enough for some basic testing.
However some basic testing is never enough for code that matters.
Minispec's helpers system allows to write tests of any complexity without sacrifice simplicity and semantic readability.
The idea is simple: if the wrapper detects a helper with same name as received message, it will will pass that message to the helper rather than to the tested object.
Helper is receiving tested object as first argument and can apply any assertions on it.
It does not mater what a helper returns. If some assertion fails inside a helper, the test that calls the helper will be marked as failed and failure will contain both test and helper's locations.
blank?
helper not defined, so blank?
message are passed to a
. If a
does not respond to blank?
, a NoMethodError
will be raised:
is(a).blank?
blank?
helper defined, so blank?
message are passed to helper rather than to a
. a
may or may not respond to blank?
:
# defining a helper
helper :blank? do |a|
# validating given object
is(a.to_s).empty?
end
# defining a test
should 'return a non-empty string' do
a = Some.abstract.string
is(a).blank?
end
↳ Built-in helpers
Minispec comes with some built-in helpers for most common scenarios: raised exceptions, thrown symbols, booleans etc.
↳ raise
helper
Without arguments any exception will be accepted:
does { some risky code }.raise
# can also be written as
does { some risky code }.raise?
does { some risky code }.raise_error?
expect { some risky code }.to_raise
expect { some risky code }.to_raise_error
When given a class it will accept only exceptions of given class:
expect { some risky code }.to_raise NoMethodError
When given a string only exceptions with same message will be accepted:
expect { some risky code }.to_raise 'some error message'
When given a Regexp only exceptions with same message as given string will be accepted:
expect { some risky code }.to_raise /some error message/
When both class and String/Regexp given, it will accept only exceptions of given class that equals/match given String/Regexp:
expect { some risky code }.to_raise NoMethodError, 'some error message'
expect { some risky code }.to_raise NoMethodError, /some error message/
When you need even more control over raised exception, use a block.
Expect any error to be raised except LoadError:
expect { something }.to_raise {|e| e.is_a?(Exception) && e.class != LoadError}
Expect raised error backtrace to contain a specific line:
expect { something }.to_raise {|e| e.backtrace.find {|l| l =~ /something/} }
↳ throw
helper
When called without arguments any thrown symbol accepted:
does { some code }.throw
# can also be written as
does { some code }.throw?
does { some code }.throw_symbol?
expect { some code }.to_throw
expect { some code }.to_throw_symbol
When called with a symbol it will pass only if given symbol thrown:
expect { some code }.to_throw :some_symbol
When called with a symbol and a value it will pass only if given symbol thrown with given value:
expect { some code }.to_throw :some_symbol, 'some value'
Also a block can be used to validate thrown symbol.
Important: when a block used, only thrown symbol passed to block, so no way to validate the value by block.
Expect any symbol to be thrown except :halt
does { some code }.throw? {|s| s != :halt}
Limitations: the code to be inspected for thrown symbols should run out of its catch
block.
This test wont pass cause thrown symbol are caught early:
describe User do
def create_account *args
catch :invalid_email do
User.new *args
end
end
it 'fails if invalid email given' do
expect { create_account(email: 'blah') }.to_throw :invalid_email
end
end
For this to work you should run User.new
outside catch
block.
↳ Boolean helpers
true?
: expects tested object to be true
:
is(a).true?
assert(a).is.true?
# same as
assert(a) == true
false?
: expects tested object to be false
:
is(a).false?
assert(a).is.false?
# same as
assert(a) == false
positive
, positive?
, truthful?,
non_falsy?: expects tested object to not be
nilnor
false`:
is(a).positive?
is(a).truthful?
expect(a).is.positive
expect(a).is.non_falsy?
↳ silent
helper
silent
(aliased as silent?
and is_silent
) expects given block to output nothing, that's it, the block should write nothing to STDOUT nor to STDERR.
is { some_code_here }.silent?
or
assert do
some
more
code
here
end.is_silent
↳ Array
helpers
same_elements
: expects tested object is an array that have same elements as given array:
a = [1, 2, :x]
b = [:x, 1, 2]
expect(a).has.same_elements_as(b)
# => passed
contain
: expects tested object is an array that contains given elements. Order does not matter:
a = [1, 2, :x]
does(a).contain? :x, 2
# => passed
does(a).contain? :y, 2
# => failed
↳ Custom helpers
The power of Minispec's helpers are revealed in full only when you define your own helpers. It is simply done by using helper
method with a block. The block will receive tested object as first argument and you can test it to the backbone:
describe Cooking do
helper :looks_like_a_pizza? do |food|
assert(food).contains? :olives, :cheese
end
it 'cooks a pizza' do
food = Cook.new.pizza
does(food).looks_like_a_pizza?
end
end
When you pass some arguments into helper they comes after tested object:
helper :ok_with_body? do |response, body|
assert(response.status) == 200
assert(response.body) == body
end
test 'index action' do
get '/'
is(last_response).ok_with_body? 'index'
end
↳ Helpers with blocks
If object passed within a block, the helper will receive that block as first argument.
Please note that the block will be received as usual argument rather than a block.
helper :blank? do |block|
is(block.call).empty?
# or is(&block).empty?
end
should 'pass' do
is { '' }.blank?
end
When a helper used with a block, the block will be passed as last argument, in form of a simple argument rather than a block:
helper :any_of? do |arr, block|
assert(arr).any?(&block)
end
should 'pass' do
has([1, 2]).any_of? {|v| v > 1}
end
↳ Helpers aliases
Often you need some helper to be accessed by various names.
Minispec allows to create helper aliases by using alias_helper
method.
Simply pass new name as first argument and existing helper name as second:
helper :open? do |door|
# ...
end
alias_helper :not_closed, :open?
it 'creates a open door' do
door = Door.new(open: true)
is(door).open?
end
it 'opens door with open!' do
door = Door.new
door.open!
assert(door).not_closed
end
Mocking
Minispec comes with a pretty full set of mocking instruments.
Though there are obvious differences, these instruments are kind of similar to ones used in another libraries like mocha, rr and rspec-mocks.
Perhaps sometimes verbose, they gives you full control over mocked objects.
↳ Expectations
Useful when you expect some object to receive some message(s).
Expectations are validated after current test evaluation finished. So the object are expected to receive given message(s) somewhere in the near future, just before current test ends.
Use to_receive
helper to add an expectation.
Expect bob to eat an apple:
apple = Apple.new
bob = Kid.new
bob.bag << apple
expect(apple).to_receive(:eaten)
↳ Expecting multiple messages
Often you need to expect multiple messages on a object.
You could of course add a expectation for each message:
expect(a).to_receive :x
expect(a).to_receive :y
but this is tedious and becomes hairy very quickly.
Recommended approach is to use :to_receive
helper with multiple arguments:
expect(a).to_receive(:x, :y)
Much better, huh?
↳ Assert given message(s) never received
Ensure a
wont receive :b
message:
expect(a).to_not.receive(:b)
same:
refute(a).receive(:b)
Ensure a
wont receive :x
message nor :y
:
assert(a).wont.receive(:x, :y)
if at least one of messages received, the test will fail.
↳ Constraints
Sometimes just checking that some messages are received is not enough. We need to know whether certain message received with certain arguments and returned/raised/thrown certain value.
Minispec allows to add such kind of constraints with ease.
↳ Arguments
Expect a
to receive :b
message with x, y arguments:
expect(a).to_receive(:b).with('x', 'y')
Expect a
to receive :b
message with whatever arguments:
expect(a).to_receive(:b).with {|*| true}
Expect a
to receive :b
message with exactly 2 arguments, whatever they are:
expect(a).to_receive(:b).with {|*a| a.size == 2}
Expect at least 2 arguments and second one to be bigger than first:
expect(a).to_receive(:b).with {|x,y| y > x}
Expect exactly 2 arguments and second one to be bigger than first:
expect(a).to_receive(:b).with do |*a|
assert(a.size) == 2
is(a.last) > a.first
end
Another way is to just return true
or false
. If block returns true, the test will pass.
expect(a).to_receive(:b).with {|*a| a.size == 2 && a.last > a.first}
↳ Arguments on multiple expectations
Expect a
to receive :x
and :y
messages with 1 and 2 arguments respectively:
expect(a).to_receive(:x, :y).with(1, 2)
for this test to pass, both a.x(1)
and a.y(2)
should be called.
You can also use a block to validate arguments.
Expect a
to receive :x
and :y
messages where x's argument is 1 and y's argument is bigger than 2:
expect(a).to_receive(:x, :y).with do |x,y|
is(x) == 1
is(y) > 2
end
Another way is to just return true
or false
. If block returns true, the test will pass.
expect(a).to_receive(:x, :y).with {|x,y| x == 1 && y > 2}
↳ Returned value
Expect a
to receive :b
message and return 'x':
expect(a).to_receive(:b).and_return('x')
Expect a
to receive :b
message and return a value bigger than 10:
expect(a).to_receive(:b).and_return {|returned_value| is(returned_value) > 10}
Another way is to just return true
or false
. If block returns true, the test will pass.
expect(a).to_receive(:b).and_return {|returned_value| returned_value > 10}
↳ Returned value on multiple expectations
Expect a
to receive :x
and :y
messages and return 1 and 2 respectively:
expect(a).to_receive(:x, :y).and_return(1, 2)
for this test to pass, a.x
should return 1 and a.y
should return 2.
If all messages expected to return same value, use a single argument.
Expect a
to receive :x
and :y
messages and both to return 1:
expect(a).to_receive(:x, :y).and_return(1)
for this to pass both :x
and :y
should return 1.
When you need full control over returned values, use a block.
Expect a
to receive :x
and :y
messages where :x
will return 1 and :y
a value bigger than 5:
expect(a).to_receive(:x, :y).and_return do |x,y|
is(x) == 1
is(y) > 5
end
Another way is to just return true
or false
. If block returns true, the test will pass.
expect(a).to_receive(:x, :y).and_return {|x,y| x == 1 && y > 5}
↳ Raised exception
When you expect a message to raise a exception, use and_raise
expectation.
Expect a
to receive :b
message and raise something:
expect(a).to_receive(:b).and_raise
When you expect a specific error, pass expected error class as first argument.
Expect a
to receive :b
message and raise NoMethodError error:
expect(a).to_receive(:b).and_raise NoMethodError
When you expect a specific error with a specific message, pass expected error class and expected message wrapped into an array.
Expect a
to receive :b
message and raise CustomError error with 'something went wrong' message:
expect(a).to_receive(:b).and_raise [CustomError, 'something went wrong']
When you need error message to match some string, use a Regexp.
Expect a
to receive :b
message and raise CustomError error with a message that match /something/
:
expect(a).to_receive(:b).and_raise [CustomError, /something/]
When you need even more control over raised exception, use a block.
Expect a
to receive :b
message and raise anything but LoadError:
expect(a).to_receive(:b).and_raise do |e|
assert(e).is_a?(Exception)
assert(e.class) != LoadError
end
Another way is to just return true
or false
. If block returns true, the test will pass.
expect(a).to_receive(:b).and_raise do |e|
e.is_a?(Exception) && e.class != LoadError
end
↳ Raised exception on multiple expectations
Expect a
to receive :x
and :y
messages and both to raise something:
expect(a).to_receive(:x, :y).and_raise
If you expect a specific error for each message to be raised, just pass expected errors as arguments.
Expect a
to receive :x
and :y
messages where :x
will raise NoMethodError error and :y
will raise StandardError:
expect(a).to_receive(:x, :y).and_raise(NoMethodError, StandardError)
If you need also to check error messages, pass error class and message wrapped into an array.
Expect a
to receive :x
and :y
messages where :x
will raise NoMethodError that match /X/ and :y
will raise StandardError that match /Y/:
expect(a).to_receive(:x, :y).and_raise([NoMethodError, /X/], [StandardError, /Y/])
It's not a sin to expect only error type on some message and error type with message on another.
Expect a
to receive :x
and :y
messages where :x
will raise NoMethodError and :y
will raise StandardError that match /Y/:
expect(a).to_receive(:x, :y).and_raise(NoMethodError, [StandardError, /Y/])
If all messages are expected to raise same error, use a single argument.
Expect a
to receive :x
and :y
messages and both to raise StandardError:
expect(a).to_receive(:x, :y).and_raise(StandardError)
It is also possible to use a block for validating raised exceptions.
The block will receive as many arguments as messages expected. Each argument will be a exception instance if its message raised something or nil
otherwise.
Expect a
to receive :x
and :y
messages where :x
will raise NoMethodError and :y
's backtrace will contain a specific line:
expect(a).to_receive(:x, :y).and_raise do |x,y|
assert(x).is_a? NoMethodError
assert(y.backtrace).any? {|l| l =~ /something/}
end
Another way is to just return true
or false
. If block returns true, the test will pass.
expect(a).to_receive(:x, :y).and_raise do |x,y|
x.is_a?(NoMethodError) && y.backtrace.any? {|l| l =~ /something/}
end
↳ Assert nothing raised
Often you need to assure some message received and nothing raised. There are without_raise
expectation that will ensure nothing raised on message receiving.
Expect a
to receive :b
and nothing raises:
expect(a).to_receive(:b).without_raise
Also works on multiple expectations. In this case the test will fail if at least one message raises a exception.
Expect a
to receive :x
and :y
without raise anything:
expect(a).to_receive(:x, :y).without_raise
↳ Thrown symbol
When you expect a symbol to be thrown, use and_throw
expectation.
Expect a
to receive :b
message and throw :x
symbol:
expect(a).to_receive(:b).and_throw :x
Note: unlike and_raise
expectation, and_throw
can not be used without arguments. It requires exactly one argument - the expected symbol(unless a block used).
Also a block can be used to validate thrown symbol. This is the case when and_throw
expectation should be used without arguments.
Expect a
to receive :b
message and throw any symbol except :x
:
expect(a).to_receive(:b).and_throw do |s|
assert(s).is_a? Symbol # making sure something actually thrown
assert(s) != :x
end
Another way is to just return true
or false
. If block returns true, the test will pass.
expect(a).to_receive(:b).and_throw {|s| s.is_a?(Symbol) && s != :x }
Limitations: unlike throw?
helper, expectations can only check for thrown symbol, so there is no way to get and validate thrown value with expectations.
↳ Thrown symbol on multiple expectations
When multiple messages expected, and_throw
method will accept same number of arguments as the number of expected messages.
Expect a
to receive :x
and :y
messages and throw :xs
and :ys
symbols respectively:
expect(a).to_receive(:x, :y).and_throw(:sx, :sy)
If all messages are expected to raise same symbol, use a single argument.
Expect a
to receive :x
and :y
messages and both to throw :halt
symbol:
expect(a).to_receive(:x, :y).and_throw(:halt)
When you need even more control over thrown symbols, use a block. The block will receive exactly same number of arguments as the number of expected messages. Each argument will be a symbol if its message thrown something of nil
otherwise.
Expect a
to receive :x
and :y
messages where :x
will thrown :ok
symbol and :y
will throw anything but :halt
symbol:
expect(a).to_receive(:x, :y).and_throw do |x,y|
is(x) == :ok
assert(y).is_a? Symbol # making sure something actually thrown
assert(y) != :halt
end
Another way is to just return true
or false
. If block returns true, the test will pass.
expect(a).to_receive(:x, :y).and_throw do |x,y|
x == :ok && y.is_a?(Symbol) && y != :halt
end
↳ Assert nothing thrown
Expect a
to receive :b
message without throw any symbol:
expect(a).to_receive(:b).without_throw
Expect a
to receive :x
and :y
messages without throw any symbol:
expect(a).to_receive(:x, :y).without_throw
↳ Yielded arguments
and_yield
expectation allow to check whether some block inside expected message yielded with specific arguments.
Expect a
to receive :b
message and :b
message to yield a block with 1, 2 arguments:
expect(a).to_receive(:b).and_yield(1, 2)
When you need more control, use a block.
Expect a
to receive :b
message and :b
message to yield a block where first argument is a string and last is a symbol:
expect(a).to_receive(:b).and_yield do |*args|
assert(a.first).is_a? String
assert(a.last).is_a? Symbol
end
Another way is to just return true
or false
. If block returns true, the test will pass.
expect(a).to_receive(:b).and_yield do |*args|
a.first.is_a?(String) && a.last.is_a?(Symbol)
end
↳ Yielded arguments on multiple expectations
Expect a
to receive :x
and :y
messages where :x
will yield a block with 1, 2 arguments and :y
will yield a bloc with :z
argument:
expect(a).to_receive(:x, :y).and_yield([1, 2], :z)
If all messages are expected to yield a block with same arguments, use a single argument on and_yield
method.
Expect a
to receive :x
and :y
messages and both :x
to yield a block with :z
argument:
expect(a).to_receive(:x, :y).and_yield(:z)
Expect a
to receive :x
and :y
messages and both to yield a block with 1, 2 arguments:
expect(a).to_receive(:x, :y).and_yield([1, 2])
Please note the arguments are wrapped into an array. If using and_yield(1, 2)
instead, the test will expect :x
to yield 1 and :y
to yield 2.
When you need full control over yielded arguments, use a block.
Expect a
to receive :x
and :y
messages where :x
will yield a block with 2 or more arguments and :y
will yield a bloc with integer only arguments:
expect(a).to_receive(:x, :y).and_yield do |*args|
assert(args.first.size) >= 2
assert(args.last).all? {|a| a.is_a? Integer}
end
Another way is to just return true
or false
. If block returns true, the test will pass.
expect(a).to_receive(:x, :y).and_yield do |*args|
args.first.size >= 2 && args.last.all? {|a| a.is_a? Integer}
end
↳ Assert nothing yielded
Expect a
to receive :b
message without yield any block:
expect(a).to_receive(:b).without_yield
Expect a
to receive :x
and :y
messages without yield any block:
expect(a).to_receive(:x, :y).without_yield
↳ Messages Amount
count
, or its alias times
, allow to check how many times a specific message was received.
Expect a
to receive :b
message exactly 2 times:
expect(a).to_receive(:b).count(2)
Expect a
to receive :b
message 2 or more times:
expect(a).to_receive(:b).count {|n| n >= 2}
↳ Amount on multiple expectations
When multiple messages expected, count
will receive an argument per each message.
Expect a
to receive :x
message 2 times and :y
message 5 times:
expect(a).to_receive(:x, :y).count(2, 5)
When all messages are expected to receive same amount of times, use a single argument.
Expect a
to receive :x
and :y
messages exactly 2 times each:
expect(a).to_receive(:x, :y).count(2)
Expect a
to receive :x
message exactly 2 times and :y
message at least once:
expect(a).to_receive(:x, :y).count {|x,y| x == 2 && y > 1}
↳ Messages Order
Unlike RSpec, ordering in Minispec works only with multiple messages.
Expect a
to receive :x
, :y
, :z
messages exactly in specified order:
expect(a).to_receive(:x, :y, :z).ordered
If given messages will be received in another order, the test will fail.
It is also possible to check whether same sequence of messages received N times.
Expect a
to receive :x
, :y
sequence exactly 2 times:
expect(a).to_receive(:x, :y).ordered(2)
for this test to pass following code expected to be executed:
a.x
a.y
a.x
a.y
if at least one message not received or received in wrong order, the test will fail.
When you need more flexibility on received sequence, use a block.
Expect a
to receive :x
, :y
sequence at least once:
expect(a).to_receive(:x, :y).ordered {|n| n >= 1}
Spies
Just like expectations, spies checks for some object to receive specific message(s). The only logical difference is that spies assumes message(s) was already received rather than expects they to be received in the future.
Also there is a technical difference - while expectations does not require any preparations on the objects, spies does. You should explicitly "attach a spy" on the object and specify what methods to spy on, let the object to behave in its way and only after that you can check whether it received expected messages.
Attaching a spy on a object is easily done via spy
method.
Checking a message was received is done via received
helper(or its sugar alias received?
).
user = User.new
spy(user, :location) # attaching spy...
user.summary
assert(user).received(:location) # checking location message received
In terms of what happens after message received spies behaves exactly as expectations:
- checks arguments message(s) was received with
with
- validates returned value(s)
and_returned
- checks for raised errors
and_raised
- checks for thrown symbols
and_thrown
- validates yielded arguments
and_yielded
- checks how many times message(s) was received
count
- checks messages was received in specific order
ordered
And just as with expectations, spies behaves well when dealing with multiple messages. Just attach a spy on multiple messages and validate them all at once:
spy(user, :name, :age, :location)
user.summary
assert(user).received(:name, :age, :location)
Stubs
Minispec allows to stub any method on a given object and have full control over stub behavior.
Add :x
stub:
stub(some_object, :x)
some_object.x
will return nil
.
When you need a stub to return some value, regardless given arguments, use a Hash or a block.
Add :x
stub and make it return :y
:
stub(some_object, :x => :y)
some_object.x
will return :y
.
Add :x
stub and make it return :z
:
stub(some_object, :x) { :z }
now some_object.x
will return :z
.
Important! Stubs does not impose any restrictions on arity, so stubbed methods can be called with any arguments!
Given arguments will just be passed into the block, preceded by the original. That's it, the block will receive the original method as first argument. If stubbed method were not defined on that object before stubbing, the block will receive nil
as first argument.
Another important note is that method's visibility are kept even after they are stubbed. So if some method exists on target object and it is protected, the stub that will override original method will be protected as well.
Same for private and public methods.
And of course if we are stubbing some object that will still exists after test finished, the stubbed methods will be restored to their originals.
↳ Argument-vary stubs
Often you need a stub to behave in a way when receiving some arguments and another way when receiving another arguments.
At a first glance this could be done by comparing arguments inside the block:
Bad!
stub(some_object, :some_method) do |orig, *args|
if args == [:a, :b]
# do this
elsif args == [:x, :y]
# do that
end
end
however this approach is tedious(at least) and really ugly.
Recommended way here is to use a block with each sequence of arguments.
For this to work you'd need to use with
method.
It takes expected arguments and a block to be yielded when the stub called with given arguments:
stub(some_object, :some_method).
with(:a, :b) { 'called with a, b' }.
with(:x, :y) { 'called with x, y' }
now some_object.some_method(:a, :b)
will return 'called with a, b' and some_object.some_method(:x, :y)
will return 'called with x, y'.
However! if called without arguments or with any arguments except [:a, :b] and [:x, :y], this example will actually return nil
.
To define a "catchall" add one more block using with_any
(or simply any
):
stub(some_object, :some_method).
with(:a, :b) { 'called with a, b' }.
with(:x, :y) { 'called with x, y' }.
with_any { 'whatever' }
now when calling some_method
without arguments or with any arguments but [:a, :b] and [:x, :y], it will return 'whatever'.
with_any
can also be used with a value rather than a block. Also it can be placed anywhere in the chain, the order does not change the result:
stub(some_object, :some_method).
with_any('whatever').
with(:a, :b) { 'called with a, b' }.
with(:x, :y) { 'called with x, y' }
One more note: if you prefer a more verbose style you can use stub
method multiple times:
stub(some_object, :some_method).with(:a, :b) { 'called with a, b' }
stub(some_object, :some_method).with(:x, :y) { 'called with x, y' }
stub(some_object, :some_method).any { 'whatever' } # or `any('whatever')`
this will work exactly the same way as chained syntax.
↳ Stubbing multiple methods at once
Often you need to stub multiple methods and you feel that calling stub
for each one is at least tedious.
And you will be right. Cause Minispec allows to add multiple stubs in one call. For this to work simply use stubs
instead of stub
.
Stub :x
, :y
and :z
methods on cube
:
stubs(cube, :x, :y, :z)
Worth to note that given block will apply to all stubs.
Stub :x
, :y
and :z
methods on cube
and make them all return the square of given value:
stubs(cube, :x, :y, :z) {|orig, n| n ** 2}
now cube.x(2)
will return 4, cube.y(4)
will return 16 etc.
Same for with
and with_any
methods - they apply to all stubbed methods without a way to different constraints for some stub:
stubs(a, :b, :c).
with(1) {:one}.
with(2) {:two}.
with_any {:whatever}
now both a.b(1)
and a.c(1)
will return :one, both a.b(2)
and a.c(2)
will return :two and any of a.b
/a.c
without arguments or with any arguments but 1 or 2 will return :whatever.
Also the method's visibility will be kept, so if some protected exists on target object, the stub will be protected as well. Same for private and public methods.
And of course multiple protected/private stubs can be defined by using protected_stubs
and private_stubs
accordingly.
↳ Chained stubs
When you need to stub a chain of methods in one statement use a string of dot separated methods:
stub(a, 'x.y.z')
now a.x.y.z
will work, though it will return nil
.
When you need last method in the chain to return some value, use Hash or a block:
stub(a, 'b.c' => :z)
now a.b.c
will return :z
.
stub(a, 'b.c') { :x }
now a.b.c
will return :x
.
A important difference from regular stubs is that chained ones wont receive the original as first argument. They will only receive the arguments passed when stub called:
stub(a, 'b.c') {|x| x ** 2}
a.b.c(4)
# => 16
If a block given when calling last method in the chain, it will be passed into the block alongside with any arguments. However you'll can not use yield
here. You should receive it as argument and call it explicitly:
stub(a, 'b.c') do |n, block|
block.call(n)
end
a.b.c(4) {|y| y ** 2}
# => 16
Important! just like regular stubs, chained ones may have arguments-vary behavior:
stub(a, 'b.c').
with(1) {:one}.
with(2) {:two}.
with_any { :whatever }
now a.b.c(1)
will return :one and a.b.c(2)
will return :two. If called without arguments or with any but 1 or 2, it will return :whatever.
Worth to note that chained stubs does not care about method visibility. It will always define a public singleton method on the target object:
So, stub(a, 'b.c')
will define b
public singleton method on a
, even if b
exists and it is protected/private.
That's the big difference from regular stubs where stubs keeps same visibility as original methods.
Please be aware that if the method to be stubbed already exists on the target object, it will be overridden for the time of test running and restored after the test finished.
↳ Calling original
If stubbed method already exists, the original method will be passed into block as first argument. Otherwise the block will receive nil
as first argument.
stub(API, :request) do |original, *args, &block|
# call the original with given args and block
original.call(*args, &block)
end
API.request
will call our stub which will then call the original.
↳ Stubs visibility
Keeping the SUT(system under test) in nearly same state as it would act in a real environment is a high priority matter for Minispec.
That's why when it is stubbing methods it is keeping original method visibility. Meant if a method were protected before stubbing, the stub will be protected as well. Same for private and public methods.
However if you want to enforce specific visibility on stubbed method, use one of public_stub
, protected_stub
or private_stub
.
They will define a stub with a specific visibility regardless the visibility of original method.
And of course there are their counterparts for multiple stubbing: public_stubs
, protected_stubs
or private_stubs
.
Mocks
Basically a mock is a mix of a stub and a expectation. Meant that you do not need to separately stub a method then add an expectation on it. Mocks doing this automatically.
Stub method :x
and ensure it will be called by the end of test:
mock(some_object, :x)
that's it.
This is a replacement for:
stub(some_object, :x)
expect(some_object).to_receive(:x)
Worth to note that expectations added by mocks are very basic ones, they will only expect message to be received. That's it, no arguments constraints, no returned value validation etc. If you need a more complex expectation you'll have to define it explicitly.
Another important note: mock
method will actually return a stub, so you have full control over stubbed method's behavior.
Mock method :x
by making it return :one
when called with 1:
mock(some_object, :x).with(1) { :one }
Mock method :x
by making it return :one
when called with 1 and return 'whatever' when called with any other arguments or without arguments at all:
mock(some_object, :x).
with(1) { :one }.
with_any { 'whatever' }
Mocks also works with Hashes.
Mock method :a
to return :x
and method :b
to return :y
:
mock(some_object, :a => :x, :b => :x)
And as with stubs you can not use arguments filters when mocked methods given as a Hash.
This will raise an ArgumentError: mock(some_object, :a => :x).with(...) {...}
.
Same for with_any
.
There is also a way to mock multiple methods at once.
For this simply use mocks
instead of mock
.
Mock :a
and :b
methods:
mock(some_object, :a, :b)
for this to pass both some_object.a
and some_object.b
should be called.
When mocking multiple methods, the returned value will apply to all methods.
Mock :a
and :b
and make them both to return :x
:
mock(some_object, :a, :b) { :x }
There is no way to have specific setups when mocking multiple methods, that's it, all of them will behave the same way.
Mock :a
and :b
and make them both to return :one
when called with argument 1 and return :two
when called with argument 2:
mock(some_object, :a, :b).
with(1) { :one }.
with(2) { :two }
Mocks visibility rules works the same as for stubs. If some protected method are mocked, the mocked version will be protected as well. Same for private and public methods.
However when you need a mock to be of specific visibility, use one of public_mock
, protected_mock
or private_mock
.
And of course there are their counterparts for multiple mocking: public_mocks
, protected_mocks
or private_mocks
.
One significant difference between mocks and stubs is that mocks does not support chained methods. That's it, you can not do like this: mock(some_object, 'a.b.c')
. Instead you should stub the chain then explicitly define expectations:
stub(some_object, 'a.b.c')
expect(some_object).to_receive(:a)
expect(some_object.a).to_receive(:b)
# ...
Doubles
During testing you may need entities that behaves like some "real" objects.
Let's say you need to ensure a welcome email is sent to user after account creation.
Rather than create a full-blown mail object you can use a double that behaves like a mailer, e.g. responds to deliver
.
To create a double simply use the double
method:
email = '[email protected]'
# creating mailer double
mailer = double(:mailer, deliver: true)
# ensuring `deliver` will be called with user's email
expect(mailer).to_receive(:deliver).with(user.email)
# injecting double into system
User.new!(email: email, mailer: mailer)
For this contrived test to pass, mailer#deliver
should be called under the hood.
If one or more arguments given to double
method, first argument will be used as name(unless it is a Hash). Double's name turns to be very helpful on failures output, so a real name will output rather than #<Object...
notation.
Running Specs
Minispec will look for specs in ./spec
and ./test
folders.
Any files that match *_spec.rb
, *_test.rb
or test_*.rb
will be loaded by Minispec.
So if you go standard way and put name you spec files like this and put them in spec
or test
folder, all you need to run specs is to call minispec
in you terminal:
$ minispec
If you want to test only some files, pass them as space delimited arguments:
$ minispec spec/user_spec.rb spec/cart_spec.rb
If you need to run specs from a script, use Minispec.run
.
Rakefile
require 'minispec'
desc 'Run all tests'
task :test do
Minispec.run
end
run
accepts pattern
option, so you can instruct Minispec on how to load your specs:
require 'minispec'
namespace :test do
desc 'Run user tests'
task :users do
Minispec.run(pattern: 'test/**/user*.rb')
end
end
Also :file
option accepted so you can run a single file:
require 'minispec'
Dir['test/**/test_*.rb'].each do |file|
name = file.sub(/test\/test_(.+)\.rb/, '\1')
desc 'Run %s tests' % name
task 'test:' + name do
Minispec.run(file: file)
end
end
Contributors
Want to contribute? Great! Contributors highly wanted and welcome!
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
License
Copyright © 2014 Slee Woo <[email protected]>
Distributed under the MIT License