Skip to content
Mike Miller edited this page Aug 19, 2021 · 7 revisions

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 (Example Groups)

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.

Test Values

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.

Nesting

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).

Special Groups

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

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.

Pending Tests

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.

Short-hand Syntax

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.

Clone this wiki locally