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?
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.
Fixtures, however, come with drawbacks:
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.
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
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.
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
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
Now it's time to commit your progress again.
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
Now it's time to commit your progress again.
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.
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
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".
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"
Now it's time to commit your progress again.
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.
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
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
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