What is Behaviour-driven Development?

Behaviour-driven Development (BDD) as a software development process is composed of multiple subtechniques. Under the hood, it combines Test Driven Development, which allows short feedback loops, with domain-driven design and object-oriented design and analysis. As a development process, it allows the stakeholders and the developers to enjoy the benefits of short feedback loops, writing just the right amount of code and design to make the software work. You can read more about BDD in our tutorial on Behavior-driven Development.

Introducing our Model

When we think of Rails models, we usually think of reflecting the problem domain for which we are providing a solution. Models can sometimes be full-blown objects with rich behaviour. Other times, they can be just simple data containers or data maps.

For the purpose of this tutorial, we will be creating a model called Auction. We will keep things simple — each auction will have only one item for sale. The Auction model will have an end date, item title, description of the item on sale, list of bids, seller and a winning bid.

Let’s generate this model using Rails’ generators:

bin/rails generate model Auction start_date:datetime end_date:datetime title:string description:text
      invoke  active_record
      create    db/migrate/20160406205337_create_auctions.rb
      create    app/models/auction.rb
      invoke    rspec
      create      spec/models/auction_spec.rb

After that, let’s run the migration and prepare the database:

rake db:migrate db:test:prepare

== 20160406205337 CreateAuctions: migrating ===================================
-- create_table(:auctions)
   -> 0.0021s
== 20160406205337 CreateAuctions: migrated (0.0023s) ==========================

For the time being, we’ll omit the bids and the sellers and focus on the title, description, start_date and end_date. Since we’ve laid the foundations, let’s get to work. The first thing we’ll tackle are the validations.

Specifying Validations

Since we are currently not sure which validations we should have in the model, let’s create some pending examples to get the ball rolling:

require 'rails_helper'

RSpec.describe Auction, :type => :model do
  it "is valid with valid attributes"
  it "is not valid without a title"
  it "is not valid without a description"
  it "is not valid without a start_date"
  it "is not valid without a end_date"
end

Here we have the basic validations specced out. The purpose of the first spec is to make it obvious what is needed to make a valid object of the Auction class. If we run this spec, we will see five pending specs:

rspec spec/models/auction_spec.rb

*****

Pending:
  Auction is valid with valid attributes
    # Not yet implemented
    # ./spec/models/auction_spec.rb:4
  Auction is not valid without a title
    # Not yet implemented
    # ./spec/models/auction_spec.rb:5
  Auction is not valid without a description
    # Not yet implemented
    # ./spec/models/auction_spec.rb:6
  Auction is not valid without a start_date
    # Not yet implemented
    # ./spec/models/auction_spec.rb:7
  Auction is not valid without a end_date
    # Not yet implemented
    # ./spec/models/auction_spec.rb:8

Finished in 0.00053 seconds (files took 2.1 seconds to load)
5 examples, 0 failures, 5 pending

Since all of the specs are pending, let’s implement the first one.

require 'rails_helper'

RSpec.describe Auction, :type => :model do
  it "is valid with valid attributes" do
    expect(Auction.new).to be_valid
  end

  it "is not valid without a title"
  it "is not valid without a description"
  it "is not valid without a start_date"
  it "is not valid without a end_date"
end

Since we have not added any constraints to the model, our model object will be valid without specifying any attributes. Let’s run the spec and see it in effect:

rspec spec/models/auction_spec.rb

.*****

Pending:
  Auction is valid with valid attributes
    # Not yet implemented
    # ./spec/models/auction_spec.rb:8
  Auction is not valid without a title
    # Not yet implemented
    # ./spec/models/auction_spec.rb:9
  Auction is not valid without a description
    # Not yet implemented
    # ./spec/models/auction_spec.rb:10
  Auction is not valid without a start_date
    # Not yet implemented
    # ./spec/models/auction_spec.rb:11
  Auction is not valid without a end_date
    # Not yet implemented
    # ./spec/models/auction_spec.rb:12

Finished in 0.00629 seconds (files took 1.36 seconds to load)
6 examples, 0 failures, 5 pending

Next, let’s implement the second example. For this example, we will need to create an Auction object without a title.

it "is not valid without a title" do
  auction = Auction.new(title: nil)
  expect(auction).to_not be_valid
end

If we run this spec now, we should see it failing.

.F***

Pending:
  Auction is not valid without a description
    # Not yet implemented
    # ./spec/models/auction_spec.rb:13
  Auction is not valid without a start_date
    # Not yet implemented
    # ./spec/models/auction_spec.rb:14
  Auction is not valid without a end_date
    # Not yet implemented
    # ./spec/models/auction_spec.rb:15

Failures:

  1) Auction is not valid without a title
     Failure/Error: expect(auction).to_not be_valid
       expected # not to be valid
     # ./spec/models/auction_spec.rb:10:in `block (2 levels) in '

Finished in 0.00774 seconds (files took 1.28 seconds to load)
5 examples, 1 failure, 3 pending

Failed examples:

rspec ./spec/models/auction_spec.rb:8 # Auction is not valid without a title

As you can see, the example failed because our validation functionality needs to be added to the model. Since we’ve completed the red step (from the red-green-refactor loop), let’s get our test passing. To get this step working, we need to add the validation on the title attribute:

class Auction < ActiveRecord::Base
  validates_presence_of :title
end

With the validation in place, we can run the tests again:

F.***

Pending:
  Auction is not valid without a description
    # Not yet implemented
    # ./spec/models/auction_spec.rb:13
  Auction is not valid without a start_date
    # Not yet implemented
    # ./spec/models/auction_spec.rb:14
  Auction is not valid without a end_date
    # Not yet implemented
    # ./spec/models/auction_spec.rb:15

Failures:

  1) Auction is valid with valid attributes
     Failure/Error: expect(Auction.new).to be_valid
       expected # to be valid, but got errors: Title can't be blank
     # ./spec/models/auction_spec.rb:5:in `block (2 levels) in '

Finished in 0.01968 seconds (files took 1.66 seconds to load)
5 examples, 1 failure, 3 pending

Failed examples:

rspec ./spec/models/auction_spec.rb:4 # Auction is valid with valid attributes

Our example is passing now, but our first example fails. This happened due to the newly introduced validation. Let's fix our first spec now:

it "is valid with valid attributes" do
  expect(Auction.new(title: 'Anything')).to be_valid
end

To get the first example passing, we need to specify the title to the Auction object, which is the subject of our example. Let's run the specs again now:

rspec spec/models/auction_spec.rb

..***

Pending:
  Auction is not valid without a description
    # Not yet implemented
    # ./spec/models/auction_spec.rb:13
  Auction is not valid without a start_date
    # Not yet implemented
    # ./spec/models/auction_spec.rb:14
  Auction is not valid without a end_date
    # Not yet implemented
    # ./spec/models/auction_spec.rb:15

Finished in 0.01081 seconds (files took 1.36 seconds to load)
5 examples, 0 failures, 3 pending

Since our specs are passing, we've reached the green step of the red-green-refactor flow. It's time to refactor our examples. This refactoring step is quite simple. We'll add a subject to our specs, which will be the main object under test in this spec file. After that, we will appropriately set the object attributes in the examples. This is what our refactored spec will look like:

require 'rails_helper'

RSpec.describe Auction, :type => :model do
  subject { described_class.new }

  it "is valid with valid attributes" do
    subject.title = "Anything"
    expect(subject).to be_valid
  end

  it "is not valid without a title" do
    expect(subject).to_not be_valid
  end

  it "is not valid without a description"
  it "is not valid without a start_date"
  it "is not valid without a end_date"
end

If we run our specs now, they will pass. Let's add the rest of the validations by writing the specs first, and the implementation after. To save up space, we will write all of the specs at once, and get them passing after that.

require 'rails_helper'

RSpec.describe Auction, :type => :model do
  subject { described_class.new }

  it "is valid with valid attributes" do
    subject.title = "Anything"
    subject.description = "Anything"
    subject.start_date = DateTime.now
    subject.end_date = DateTime.now + 1.week
    expect(subject).to be_valid
  end

  it "is not valid without a title" do
    expect(subject).to_not be_valid
  end

  it "is not valid without a description" do
    subject.title = "Anything"
    expect(subject).to_not be_valid
  end

  it "is not valid without a start_date" do
    subject.title = "Anything"
    subject.description = "Lorem ipsum dolor sit amet"
    expect(subject).to_not be_valid
  end

  it "is not valid without a end_date" do
    subject.title = "Anything"
    subject.description = "Lorem ipsum dolor sit amet"
    subject.start_date = DateTime.now
    expect(subject).to_not be_valid
  end
end

Our specs are done, so let's run them:

rspec spec/models/auction_spec.rb

.....

Finished in 0.01619 seconds (files took 1.33 seconds to load)
5 examples, 0 failures

Although the specs are working, as you can notice, we have quite a lot of duplication. Let's see how we can refactor these examples to make them more concise.

Refactoring and Getting back to Red

The RSpec testing framework provides some quite useful utilities. We have seen one of them already — the subject method. We should see the subject as the testing subject — meaning that it will be the object upon which our examples will set expectations.

require 'rails_helper'

RSpec.describe Auction, :type => :model do
  subject {
    described_class.new(title: "Anything", description: "Lorem ipsum",
                      start_date: DateTime.now, end_date: DateTime.now + 1.week)
  }

  it "is valid with valid attributes" do
    expect(subject).to be_valid
  end

  it "is not valid without a title" do
    subject.title = nil
    expect(subject).to_not be_valid
  end

  it "is not valid without a description" do
    subject.description = nil
    expect(subject).to_not be_valid
  end

  it "is not valid without a start_date" do
    subject.start_date = nil
    expect(subject).to_not be_valid
  end

  it "is not valid without a end_date" do
    subject.end_date = nil
    expect(subject).to_not be_valid
  end
end

By making our subject valid, our examples will get much simpler. For every example we can remove the attribute that we would like to assert upon. If we run the tests now, we'll see that they are still passing:

rspec spec/models/auction_spec.rb

.....

Finished in 0.01468 seconds (files took 1.34 seconds to load)
5 examples, 0 failures

Now, let's go back and remind ourselves of what attributes and relationships an Auction object should have. Since an auction in fact means a sale, we need a person that will sell the item and a person that will buy the item. In other words, an Auction should have a buyer and a seller.

Since both of these associations represent a person, we'll need to introduce a User model, and add the appropriate associations to the Auction. Instead of going in-depth with the User model and its specs, we'll go over them quickly, and continue with the implementation of the Auction model.

Here is our model:

# app/models/user.rb
class User < ApplicationRecord
  validates_presence_of :password, :email
end

Next, let's take a look at the spec for the User model:

require 'rails_helper'

RSpec.describe User, :type => :model do
  subject { described_class.new(password: "some_password", email: "john@doe.com") }

  describe "Validations" do
    it "is valid with valid attributes" do
      expect(subject).to be_valid
    end

    it "is not valid without a password" do
      subject.password = nil
      expect(subject).to_not be_valid
    end

    it "is not valid without an email" do
      subject.email = nil
      expect(subject).to_not be_valid
    end
  end
end

Having this in place, we need to add appropriate specs for the seller and buyer associations to the Auction:

describe "Associations" do
  it "has one buyer" do
    assc = described_class.reflect_on_association(:buyer)
    expect(assc.macro).to eq :has_one
  end

  it "has one buyer" do
    assc = described_class.reflect_on_association(:buyer)
    expect(assc.macro).to eq :has_one
  end
end

One way to test associations for a model is by using the reflect_on_association method, which will return information about the given association. Then, we can set expectations on the result of the #macro method.

An alternative is to use the shoulda gem. By adding this gem to our bundle, we can easily test our associations using:

describe "Associations" do
  it { should have_one(:buyer) }
  it { should have_one(:seller) }
end

If we run our specs at this point, they will fail. Let's make them pass by adding the appropriate associations in our Auction model:

class Auction < ActiveRecord::Base
  has_one :buyer, class_name: "User"
  has_one :seller, class_name: "User"

  validates_presence_of :title, :description, :start_date, :end_date
end

If we run our tests now, they will pass:

➜ rspec
..........

Finished in 0.03549 seconds (files took 2.04 seconds to load)
10 examples, 0 failures

Having the buyer and seller associations in place, we can think about bidding. An auction can have multiple bids. While auctions are a very interesting and difficult problem to solve, for the purpose of this tutorial, we will add another model which will be called Bid.

Adding Business Behaviour

If you think about real-life auctions, you will immediately think about the auctioneer chants. The integral part of these chants are the bids made by the bidders. An Auction can have multiple bids, which, when put into code, translates to a new association. Also, a Bid can have only one bidder, which will be an association by itself.

Let's introduce the Bid model:

➜  rails g model Bid bidder_id:integer auction_id:integer amount:integer
Running via Spring preloader in process 89375
      invoke  active_record
      create    db/migrate/20160413214616_create_bids.rb
      create    app/models/bid.rb
      invoke    rspec
      create      spec/models/bid_spec.rb

The model should have only one validation — it should validate the presence of a bidder:

RSpec.describe Bid, :type => :model do
  describe "Associations" do
    it { should belong_to(:bidder) }
    it { should belong_to(:auction) }
  end

  describe "Validations" do
    it { should validate_presence_of(:bidder) }
  end
end

If we run this spec now, it will fail:

➜  rspec spec/models/bid_spec.rb

Failures:

  1) Bid Validations should validate that :bidder cannot be empty/falsy
     Failure/Error: it { should validate_presence_of(:bidder) }
     Shoulda::Matchers::ActiveModel::AllowValueMatcher::AttributeDoesNotExistError:
       The matcher attempted to set :bidder on the Bid to nil, but that
       attribute does not exist.
     # ./spec/models/bid_spec.rb:5:in `block (3 levels) in <top (required)>'

To make this test pass, we only need to add the association and the validation to the model:

class Bid < ApplicationRecord
  belongs_to :bidder, class_name: "User"

  validates_presence_of :bidder
end

This will make our tests pass. Now, to add the necessary business logic, we first need to think about the bidding process. The core business logic around bidding requires that each bid must be a bigger than the previous bid. This means that every Auction will have multiple Bids. Let's add a test for that association and introduce it to the model.

We will again use the shoulda matcher for the test:

# auction_spec.rb
describe "Associations" do
  it { should belong_to(:buyer) }
  it { should belong_to(:seller) }
  it { should have_many(:bids) }
end

If we run the spec now, the tests will fail. We need to introduce the association:

class Auction < ActiveRecord::Base
  belongs_to :buyer, class_name: "User", optional: true # Rails 5!
  belongs_to :seller, class_name: "User"
  has_many :bids

  validates_presence_of :title, :description, :start_date, :end_date
end

Our specs will carry on failing because we will need to have a buyer and a seller for our test subject. We also need to add the a buyer_id and a seller_id fields to the Auction. The foreign keys can be easily added using a migration, so we will skip that part here. To get our specs back to a green state, we need to update our subject. This means that we need to create one User object — one seller, and assign it to the subject Auction:

let(:seller) { User.new(:email => "jane@doe.com", :password => "pw1234",
                       :password_confirmation => "pw1234") }
subject {
  described_class.new(title: "Anything", description: "Lorem ipsum",
                    start_date: DateTime.now, end_date: DateTime.now + 1.week,
                    seller: seller)
}

To add the business logic, we need a validation. After adding a new Bid to the Auction, we need to check if the bid is bigger than the previous one. Because of the intersection between the Auction and the Bid objects, we will create an additional class, which will contain all of the bidding logic required for an Auction. We'll call it BiddingEngine. First, let's add the spec for it:

# specs/models/bidding_engine_spec.rb
require 'rails_helper'

RSpec.describe BiddingEngine do
  describe ".bid!" do
    it "create a new bid on an auction" do
      seller = User.create(:email => 'jane@doe.com', :password => 'pw1234',
                         :password_confirmation => 'pw1234')
      bidder = User.create(:email => 'john@doe.com', :password => 'pw1234',
                         :password_confirmation => 'pw1234')

      auction = Auction.create(title: 'Anything', description: 'Lorem ipsum',
                   start_date: DateTime.now, end_date: DateTime.now + 1.week,
                   seller_id: seller.id)

      described_class.bid!(auction, 100, bidder)
      expect(auction.errors).to be_empty

      described_class.bid!(auction, 90, bidder)
      expect(auction.errors[:bid].first).to eq "must be bigger than the last bid on the auction"
    end
  end
end

The BiddingEngine class will have the bid! method, which will contain the required logic to set a new bid on the Auction. Using this class, we will have only a single entry point towards the, often fragile, bidding process. By having this in place, we'll have an overview of how the bidding process is done.

Let's add the implementation of this class:

class BiddingEngine
  def self.bid!(auction, amount, bidder)
    new(auction, amount, bidder).bid!
  end

  def initialize(auction, amount, bidder)
    @auction = auction
    @bid = Bid.new(bidder: bidder, auction: @auction, amount: amount)
  end

  def bid!
    if @bid.valid? && is_bigger?
      @bid.save
    else
      @auction.errors.add(:bid, "must be bigger than the last bid on the auction")
    end
  end

  private

  def is_bigger?
    return true unless @auction.bids.last
    @auction.bids.last.amount < @bid.amount
  end
end

As you can see, the implementation of the class is quite simple. It will only have the bid! method, and it will perform only one validation, which is the business behaviour that we wanted to achieve.

Some of you might go down the road of enabling accepts_nested_attributes_of for Bid in the Auction class, but it's easier to have this logic separated into a class. It will add more separation and better isolation. Also, using the BiddingEngine class in the controllers will be easier.

Covering Edge Cases

When a User creates a new the Bid on the Auction, we always check if it's bigger than the last bid in the auction. However, we also need to take care of the edge case where the user tries to add a bid with the same amount as the last bid.

This is an edge case, but we still have to think about it and solve it. Otherwise, we will have conflicting winners in the auctions. Let's write a spec about this behaviour, and then fix the functionality. In the test for the BiddingEngine, we will need to add another expectation stating that a Bid cannot be equal with the previous Bid on the Auction.

it 'cannot create a bid if it's an equal amount as the last bid' do
  seller = User.create(:email => 'jane@doe.com', :password => 'pw1234',
		     :password_confirmation => 'pw1234')
  bidder = User.create(:email => 'john@doe.com', :password => 'pw1234',
		     :password_confirmation => 'pw1234')

  auction = Auction.create(title: 'Anything', description: 'Lorem ipsum',
	       start_date: DateTime.now, end_date: DateTime.now + 1.week,
	       seller_id: seller.id)

  described_class.bid!(auction, 100, bidder)
  expect(auction.errors).to be_empty

  described_class.bid!(auction, 100, bidder)
  expect(auction.errors[:bid].first).to eq "must be bigger than the last bid on the auction"
end

As you can see, our spec is almost the same as the previous one. It will check for the validation errors on the Auction, stating that the Bid must be a higher amount. Now, we can easily refactor our specs by using RSpec's let statements:

require 'rails_helper'

RSpec.describe BiddingEngine do
  describe ".bid!" do
    let(:seller) { User.create(:email => 'jane@doe.com', :password => 'pw1234',:password_confirmation => 'pw1234') }
    let(:bidder) { User.create(:email => 'john@doe.com', :password => 'pw1234', :password_confirmation => 'pw1234') }
    let(:auction) { Auction.create(title: 'Anything', description: 'Lorem ipsum', start_date: DateTime.now, end_date: DateTime.now + 1.week, seller_id: seller.id) }

    it "create a new bid on an auction if bid is bigger than last bid on auction" do
      described_class.bid!(auction, 100, bidder)
      expect(auction.errors).to be_empty

      described_class.bid!(auction, 90, bidder)
      expect(auction.errors[:bid].first).to eq "must be bigger than the last bid on the auction"
    end

    it 'cannot create a bid if it's an equal amount as the last bid' do
      described_class.bid!(auction, 100, bidder)
      expect(auction.errors).to be_empty

      described_class.bid!(auction, 100, bidder)
      expect(auction.errors[:bid].first).to eq "must be bigger than the last bid on the auction"
    end
  end
end

As you can notice, the specs now look much tidier and easier to understand. We also covered an edge case, and these specs help us to prevent breaking the bidding flow and the validation of the auctions.

Conclusion

In this tutorial, we saw how we can approach testing models in Ruby on Rails. As you can see, model specs are very different from the kind of specs we would write for a regular Ruby object. Aside from theDSL that Rails provides for the model, RSpec also provides us with model specs, which allow us to properly unit test our models. We saw how we can tackle issues the BDD way, by using specs and test-driven development.

What is your preferred way to test models? Do you use BDD in your everyday work? When testing, do you use the red-green-refactor cycle? Feel free to leave a comment below.

P.S. Would you like to learn how to build sustainable Rails apps and ship more often? We've recently published an ebook covering just that — "Rails Testing Handbook". Learn more and download a free copy.