-
Notifications
You must be signed in to change notification settings - Fork 4
Structure
A spec is composed of two main pieces: contexts and examples. Contexts are also referred to as example groups.
All examples on this page will show the entire spec file to reduce confusion as to where the various parts can be used.
Here's a basic spec:
require "./spec_helper"
# Top-level context/example group.
Spectator.define MyCoolClass do
# Nested context/example group.
describe "#do_something" do
# Subject desclaration.
subject { described_class.do_something }
# Example.
it "does something" do
# Assertion.
is_expected.to_not be_nil
end
end
end
Contexts (also known as example groups), are used to group tests by methods and scenarios.
The two main keywords for defining a context are describe
and context
.
The two can be used interchangeably, but as a general rule of thumb:
Use describe
when the group describing a class, method, or return value.
Use context
when explaining a situation or scenario the code is being tested for.
For instance:
require "./spec_helper"
# describe is used because we're describing a type (String).
Spectator.describe String do
# context is used because we're explaining a state.
context "when empty" do
# describe is used because we're describing a method (#empty?).
describe "#empty?" do
it "is true" do
# ...
end
end
end
# Different situation.
context "when not empty" do
describe "#empty?" do
it "is false" do
# ...
end
end
end
end
The top-level context must be prefixed with Spectator.
This is because Spectator does expose the DSL to the top-level namespace.
Inner contexts should not be prefixed.
Typically, there is a spec file per type in the shard, and the top-level describe
refers to that type.
Sometimes tests need constructed objects to test with. And some times constructing those is tedious. When multiple tests need the same object, this is a huge chore. This is where test values come in.
Test values allow assigning a name to an expression.
Then the expression can be referenced by name.
The object created by the expression is lazily initialized.
Typically, test values are created with the let
and subject
keywords.
Helper methods could also be used for this.
require "./spec_helper"
Spectator.describe String do
let(normal_string) { "foobar" }
let(empty_string) { "" }
describe "#empty?" do
subject { string.empty? }
context "when empty" do
let(string) { empty_string }
it "is true" do
is_expected.to be_true
end
end
context "when not empty" do
let(string) { normal_string }
it "is false" do
is_expected.to be_false
end
end
end
end
Notice in the previous example that string
was defined after it was used by subject
.
This is allowed as long as everything is defined eventually before the test uses it.
Additionally, values defined by outer contexts can be used by nested contexts.
For more information on values, see the documentation on subject.
Contexts can be arbitrarily nested. This allows for flexibility when defining scenarios, sub-scenarios, and how code responds to those scenarios. Take for instance:
require "./spec_helper"
Spectator.describe LoginPage do
let(username) { 'bob' }
let(password) { 'password' }
before_each { create_user(username, password) }
after_each { delete_user(username) }
context "when 'Remember me' is checked" do
before_each { subject.remember_me = true }
# ...
end
context "when given a valid username" do
before_each { subject.username = username }
context "when given a correct password" do
before_each { subject.password = password }
# ...
end
context "when given an incorrect password" do
before_each { subject.password = 'incorrect' }
# ...
end
end
context "when given an invalid username" do
before_each { subject.username = 'jim' }
# ...
end
end
Nested example groups can be quite elaborate.
This helps builds up situations to test code,
instead of rebuilding the situation slightly differently in every test.
For additional information on before_each
and after_each
, see the documentation on hooks.
Contexts cannot be nested inside of examples (it
block).
In addition to context
and describe
, there are some contexts that do more than group examples.
The first is sample
.
A sample
block repeats the tests in it multiple times with input values.
All examples in the context are repeated, but given a different value for each test.
require "./spec_helper"
Spectator.describe Formatter do
# List of integers to test against.
def various_integers
[-7, -1, 0, 1, 42]
end
# Repeat nested tests for every value in `#various_integers`.
sample various_integers do |int|
# Example that checks if a fictitious method `#format` converts to strings.
it "formats correctly" do
expect(Formatter.format(int)).to eq(int.to_s)
end
end
end
See the documentation on sample for more information.
Another special context is used to DRY up tests dramatically.
The provided
keyword is used for this.
There are some cases where a few input values can determine how multiple methods behave.
However, writing all of these with with nested contexts and subjects gets very repetitive.
require "./spec_helper"
Spectator.describe User do
subject(user) { User.new(age) }
# Each expression in the `given` block is its own test.
provided age: 10 do
expect(user.can_drive?).to be_false
expect(user.can_vote?).to be_false
end
provided age: 16 do
expect(user.can_drive?).to be_true
expect(user.can_vote?).to be_false
end
provided age: 18 do
expect(user.can_drive?).to be_true
expect(user.can_vote?).to be_true
end
end
See the documentation on provided for more information.
Examples are meat of a spec.
They're the blocks that test that the code does what it should.
An example is defined by using it
.
Additionally, specify
can be used instead, depending on your needs.
A description follows and then the block of code designating the test.
The description is optional, but recommended.
require "./spec_helper"
Spectator.describe Bytes do
it "stores an array of bytes" do
bytes = Bytes.new(32)
bytes[0] = 42
expect(bytes[0]).to eq(42)
end
end
Each test should contain at least one expectation/assertion. For details on them, see the documentation on expectations. Expectations can exist inside example blocks and conditions.
Tests can be skipped by changing it
to pending
.
If you prefer, skip
and xit
can also be used, which are used by RSpec.
require "./spec_helper"
Spectator.describe Nil do
pending "this is skipped" do
# ...
end
end
The test in the example will not run. This can be useful to stub tests for features that are not complete.
The it
syntax can be shortened for one-liner tests.
require "./spec_helper"
Spectator.describe String do
let(string) { "foobar" }
describe "#upcase" do
subject { string.upcase }
# Short-hand for one-liner.
it { is_expected.to eq("FOOBAR") }
# Another variant, must `require spectator/should`.
it { should eq("FOOBAR") }
end
end
The description is generated from the expectation.