Nested contexts can help you here, but keep it shallow (typically one level deep). There are two variables to consider in each example: givens (starting state) and what method is being invoked. You can group things by method or state:
# by method
describe Stack do
describe "#push" do
it "adds an element to an empty stack"
it "adds an element to a non-empty stack"
end
describe "#pop" do
it "returns nil from an empty stack"
it "returns the last element of a non-empty stack"
it "removes the last element from a non-empty stack"
end
end
# by state
describe Stack do
context "when empty" do
specify "push adds an element"
specify "pop returns nil"
end
context "when not empty" do
specify "push adds an element"
specify "pop returns last element"
specify "pop removes last element"
end
end
I've used both approaches and seen them both work really well and really badly. The key to either approach is that the examples tell a story as you read from top to bottom. As requirements evolve, this means you need to review this file, just as you do your implementation code.
An easy way to check that the spec makes sense is to run it with the documentation formatter:
rspec stack_spec.rb --format documentation
This spits out all the names in order (provided you're not using --order rand):
Stack
#push
adds an element to an empty stack
adds an element to a non-empty stack
#pop
returns nil from an empty stack
returns the last element of a non-empty stack
removes the last element from a non-empty stack
or
Stack
when empty
push adds an element
pop returns nil
when not empty
push adds an element
pop returns last element
pop removes last element
Once you see this output, it'll be pretty clear to you whether the organization you're using makes sense or not.