Factories

Generating Test Data

In our previous lesson, you may have thought that it is too cumbersome to create test data, even after we use "context" and "before" block.

 

For a programming framework that focuses on programmer happiness, there have to be other way to do it right?

Fixtures

By default, Rails provides us with a means to quickly generate sample data called "fixtures". A fixture is formatted in YAML and looks like this:

# Given our model,
# this is what foods.yml normally look like
food1:
  name: "Nasi Uduk"
  description: "Betawi style steamed rice cooked in coconut milk. Delicious!"
  price: 10000.0

Once defined, we can referencing "food(:food1)" in a test and we will get new instance of "Food" with attributes set.

Drawbacks

Fixtures, however, come with drawbacks:

  1. Fixtures can be brittle and easily broken, this will make us spending too much time maintaining our test data
  2. Fixtures bypass ActiveRecord when loaded into tests, this means that it ignores model validations that we actually need to test

Factories

Thus came factories. Promised as simple and flexible building blocks for test data, factories have their own drawbacks too. We will discuss about them later. But for now, let's try it out.

Your First Factory

To add factories to your specs for "Food" model, create a file named "spec/factories/food.rb" and fill it with these lines:

FactoryGirl.define do
  factory :food do
    name "Nasi Uduk"
    description "Betawi style steamed rice cooked in coconut milk. Delicious!"
    price 10000.0
  end
end

Adding It to Spec

Now, back to your "Food" spec, add this example:

# -cut-
describe Food do
  it "has a valid factory" do
    expect(FactoryGirl.build(:food)).to be_valid
  end
  # -cut-
end

Afterward, execute "rspec --format documentation" in your console.

More Usage in Your Spec

Now you can use your factory in other examples:

# -cut-
describe Food do
  # -cut-
  it "is valid with a name and description" do
    expect(FactoryGirl.build(:food)).to be_valid
  end

  it "is invalid without a name" do
    food = FactoryGirl.build(:food, name: nil)
    food.valid?
    expect(food.errors[:name]).to include("can't be blank")
  end

  it "is invalid without a description" do
    food = FactoryGirl.build(:food, description: nil)
    food.valid?
    expect(food.errors[:description]).to include("can't be blank")
  end
  # -cut-
end

Persisting Factory

How about factories with persisted state? Worry not:

describe Food do
  # -cut-
  it "is invalid with a duplicate name" do
    food1 = FactoryGirl.create(:food, name: "Nasi Uduk")
    food2 = FactoryGirl.build(:food, name: "Nasi Uduk")

    food2.valid?
    expect(food2.errors[:name]).to include("has already been taken")
  end
  # -cut-
end

Commit!

Now it's time to commit your progress again.

Simplifying Syntax

Let's admit it, we hate typing any more than we have to. That includes typing "FactoryGirl" over and over again. We can simplify this by configuring our RSpec configuration located in "spec/rails_helper.rb".

RSpec.configure do |config|
  # -cut-

  config.include FactoryGirl::Syntax::Methods
end

Now we can edit our "Food" specs to make it even simpler.

describe Food do
  it "has a valid factory" do
    expect(build(:food)).to be_valid
  end

  it "is valid with a name and description" do
    expect(build(:food)).to be_valid
  end

  it "is invalid without a name" do
    food = build(:food, name: nil)
    food.valid?
    expect(food.errors[:name]).to include("can't be blank")
  end

  it "is invalid without a description" do
    food = build(:food, description: nil)
    food.valid?
    expect(food.errors[:description]).to include("can't be blank")
  end

  it "is invalid with a duplicate name" do
    food1 = create(:food, name: "Nasi Uduk")
    food2 = build(:food, name: "Nasi Uduk")

    food2.valid?
    expect(food2.errors[:name]).to include("has already been taken")
  end
  # -cut-
end

Commit!

Now it's time to commit your progress again.

Fake It to Make It

Let's say that in one of your example, you need a lot of test data. For one or two test data for "Food" specs, you can easily google different names of food and their description. But what if you need 100 test data?

 

We can use Faker!

 

Ported from Perl, Faker is a library for generating fake names, addresses, sentences, etc. You can see the complete list of things it can fake here: https://github.com/stympy/faker.

Adding It to the Factories

This is how you add Faker to your factories:

FactoryGirl.define do
  factory :food do
    name { Faker::Food.dish }
    description "Betawi style steamed rice cooked in coconut milk. Delicious!"
    price 10000.0
  end
end

Updating Gemfile

Since Faker::Food is a new addition, we need to update our Gemfile.

# -cut-
group :test do
  gem 'faker', git: 'git@github.com:stympy/faker.git'
  # -cut-
end
# -cut-

Don't forget to run "bundle install".

What Faker::Food Can Do

You can try to run this from your rails console. Execute "rails c test" to load your rails console in test environment. See what you get.

Faker::Food.dish #=> "Caesar Salad"

Faker::Food.ingredient #=> "Sweet Potato"

Faker::Food.spice #=> "Caraway Seed"

Faker::Food.measurement #=> "1/4 tablespoon"

Faker::Food.metric_measurement #=> "centiliter"

Commit!

Now it's time to commit your progress again.

Drawbacks

We have mentioned earlier that factories have their drawbacks too. Unchecked factory usage, especially when we use association factories (which we have not used yet so far), factories can cause a test suite to slow down. 

Exercise

  1. Use factories for test data to our specs regarding "price" and "image_url"
  2. Use factories for test data to our specs "filter name by letter"

Solutions (1)

1.a. Using factories in our price specs

# -cut-
describe Food do
  # -cut-
  it "is valid with numeric price greater or equal to 0.01" do
    expect(build(:food, price: 0.01)).to be_valid
  end

  it "is invalid without numeric price" do
    food = build(:food, price: "abc")
    food.valid?
    expect(food.errors[:price]).to include("is not a number")
  end

  it "is invalid with price less than 0.01" do
    food = build(:food, price: -10)
    food.valid?
    expect(food.errors[:price]).to include("must be greater than or equal to 0.01")
  end
  # -cut-
end

Solutions (2)

1.b. Using factories in our image_url specs

# -cut-
describe Food do
  # -cut-
  it "is valid with image_url ending with '.gif', '.jpg', or '.png'" do
    expect(build(:food, image_url: "food.jpg")).to be_valid
  end

  it "is invalid with image_url ending not with '.gif', '.jpg', or '.png'" do
    food = build(:food, image_url: "food.csv")
    food.valid?
    expect(food.errors[:image_url]).to include("must be a URL for GIF, JPG or PNG image.")
  end
  # -cut-
end

Solutions (3)

2. Using factories for test data to our specs "filter name by letter"

# -cut-
describe Food do
  # -cut-
  describe "filter name by letter" do
    before :each do
      @food1 = create(:food, name: "Nasi Uduk")
      @food2 = create(:food, name: "Kerak Telor")
      @food3 = create(:food, name: "Nasi Semur Jengkol")
    end

    context "with matching letters" do
      it "returns a sorted array of results that match" do
        expect(Food.by_letter("N")).to eq([@food3, @food1])
      end
    end

    context "with non-matching letters" do
      it "omits results that do not match" do
        expect(Food.by_letter("N")).not_to include(@food2)
      end
    end
  end
end

[Go-Jek x BNCC] Factories

By qblfrb

[Go-Jek x BNCC] Factories

  • 391