#describe / context

describe and context define a group of related examples. They're aliases. Pick whichever reads better in plain English. Groups can be nested arbitrarily, and Raku scoping rules apply inside each block, so a my $foo declared in a group is visible to its examples and nested groups.

use BDD::Behave;

describe 'User#full-name', {
  context 'when both names are present', {
    it 'joins them with a space', {
      my $u = User.new(:first, :last);
      expect($u.full-name).to.be('Ada Lovelace');
    }
  }

  context 'when only the first name is present', {
    it 'returns the first name alone', {
      my $u = User.new(:first);
      expect($u.full-name).to.be('Ada');
    }
  }
}
Tip. context reads naturally for branching scenarios ("when X", "with Y"). Use describe for the thing under test ("User#full-name", "the parser").

#it

Each it is an example: a single behavior the surrounding group asserts. Pass a description and a block. Auto-derived descriptions are also supported: omit the string and Behave infers it from the matcher.

describe 'String#uc', {
  it 'uppercases ASCII', {
    expect('hi'.uc).to.be('HI');
  }

  # auto-described, description derived from the expectation
  it { expect('abc'.uc).to.be('ABC') }

  # pending, body is not executed, marked with a reason
  pending 'works on accented characters', 'pending Unicode fold work', {
    expect('café'.uc).to.be('CAFÉ');
  }
}

#let / let-bang

let defines a value that is lazy (only computed when first read) and memoized per example (reset between examples). Use it for the test subject and any inputs whose construction you'd otherwise repeat.

describe 'cart totals', {
  let(:cart, { ShoppingCart.new });
  let(:item, { Item.new(:price(9.99)) });

  it 'is zero when empty', {
    expect(:cart.total).to.be(0);
  }

  it 'adds the item price', {
    :cart.add(:item);
    expect(:cart.total).to.be(9.99);
  }

  context 'with a coupon', {
    # inner let shadows the outer one
    let(:cart, { ShoppingCart.new(:coupon('10OFF')) });

    it 'applies the discount', {
      :cart.add(:item);
      expect(:cart.total).to.be(8.99);
    }
  }
}

let-bang is the Raku-friendly spelling of RSpec's let!. It defines the same memoized value, but forces it before every example body so any side effects in the block run whether or not the example reads the value. Useful when the creation is itself the setup (inserting a row, recording a fixture).

describe 'invoices index', {
  let-bang(:invoice, { Invoice.create(:amount(100)) });

  it 'returns the invoice', {
    # passes even though we never read :invoice, the row exists
    expect(Invoice.all.elems).to.be(1);
  }
}

#subject / subject-bang

subject is a specialized let: the thing this group is testing. Use the bare .is / .is-not matchers for one-liners, or refer to :subject by name.

describe 'Account', {
  subject { Account.new(:balance(100)) }

  it 'starts with the configured balance', {
    expect(:subject.balance).to.be(100);
  }

  it { is.an-instance-of(Account) }
}

#hooks

Hooks run on a predictable schedule around every example or group.

HookRuns
before-eachBefore each example in the group (and nested groups).
before-allOnce before the first example in the group.
after-eachAfter each example, even on failure.
after-allOnce after the last example in the group.
around-eachWraps each example body. Must invoke the continuation to actually run it.
around-allWraps the entire group.
describe 'database fixtures', {
  before-all  { DB.connect }
  after-all   { DB.disconnect }

  before-each { DB.begin-transaction }
  after-each  { DB.rollback }

  around-each -> &example {
    my $t0 = now;
    example();
    note "took { now - $t0 }s";
  }

  it 'inserts rows', { User.create(:name); expect(User.all.elems).to.be(1) }
  it 'cleans up between examples', { expect(User.all.elems).to.be(0) }
}
Hook ordering. before-each, let-bang, and any other registered hooks run in declaration order. No surprises.

#shared contexts & examples

Factor common setup into a shared-context. Share an entire example suite across implementations with shared-examples. Both compose cleanly with the surrounding group.

shared-context 'authenticated user', {
  let(:user, { User.create(:name) });
  before-each { session.login(:user) }
}

describe 'dashboard', {
  include-context 'authenticated user';

  it 'greets the user', {
    expect(dashboard.greeting).to.include(:user.name);
  }
}
shared-examples 'a Collection', -> %params {
  let(:collection, %params);

  it 'reports its size', { expect(:collection.size).to.be(0) }
  it 'iterates', { expect(:collection.iterator.list).to.be(()) }
}

describe 'LinkedList', {
  include-examples 'a Collection', :factory({ LinkedList.new });
}

describe 'ArrayList', {
  include-examples 'a Collection', :factory({ ArrayList.new });
}

#tags, focus & skip

Tag examples and groups for arbitrary filtering on the command line. fit / fdescribe focus the run on a single example or group. xit / xdescribe skip them.

describe 'integration', :tag, {
  it 'talks to the database', :tag, {
    expect(DB.ping).to.be(True);
  }

  fit 'this one only, for now', {
    expect(1 + 1).to.be(2);
  }

  xit 'temporarily off', {
    expect(...).to.be(...);
  }
}
# run only :slow examples
behave --tag slow specs/

# exclude :integration examples
behave --tag '~integration' specs/

# example filter by description regex
behave --example 'full-name' specs/users-spec.raku