We already have a model called "Food". In the future, we will write the test first before we create the model. Right now, let's just write our first model for "Food" class.
Create a new file "spec/models/food_spec.rb" and fill it with these lines:
require 'rails_helper'
describe Food do
it "is valid with a name and description"
it "is invalid without a name"
it "is invalid without a description"
it "is invalid with a duplicate name"
end
Then execute "rspec" command in your console. You should see result similar to this:
****
Pending:
Food is valid with a name and description
# Not yet implemented
# ./spec/models/food_spec.rb:4
Food is invalid without a name
# Not yet implemented
# ./spec/models/food_spec.rb:5
Food is invalid without a description
# Not yet implemented
# ./spec/models/food_spec.rb:6
Food is invalid with a duplicate name
# Not yet implemented
# ./spec/models/food_spec.rb:7
Finished in 0.00074 seconds (files took 6.14 seconds to load)
4 examples, 0 failures, 4 pending
Now let's start from our first example.
# -cut-
describe Food do
it "is valid with a name and description" do
food = Food.new(
name: "Nasi Uduk",
description: "Betawi style steamed rice cooked in coconut milk. Delicious!",
price: 10000.0
)
expect(food).to be_valid
end
# -cut-
end
We use RSpec's "be_valid" matcher to verify that our model knows what it has to look like to be valid.
Now run "rspec" from your console again.
Next, we should verify that our model returns error without mandatory fields.
# -cut-
describe Food do
# -cut-
it "is invalid without a name" do
food = Food.new(
name: nil,
description: "Betawi style steamed rice cooked in coconut milk. Delicious!",
price: 10000.0
)
food.valid?
expect(food.errors[:name]).to include("can't be blank")
end
# -cut-
end
Now complete the third example to verify that our model returns if "description" field is not filled.
It should looks something like this:
# -cut-
describe Food do
# -cut-
it "is invalid without a description" do
food = Food.new(
name: "Nasi Uduk",
description: nil,
price: 10000.0
)
food.valid?
expect(food.errors[:description]).to include("can't be blank")
end
# -cut-
end
For our last example, we will make sure that our model rejects entries with duplicate name.
# -cut-
describe Food do
# -cut-
it "is invalid with a duplicate name" do
food1 = Food.create(
name: "Nasi Uduk",
description: "Betawi style steamed rice cooked in coconut milk. Delicious!",
price: 10000.0
)
food2 = Food.new(
name: "Nasi Uduk",
description: "Just with a different description.",
price: 10000.0
)
food2.valid?
expect(food2.errors[:name]).to include("has already been taken")
end
# -cut-
end
It's time to commit your progress to your Github repository again. By now you should already be familiar with the commands.
Apparently your new app is a hit! The product manager comes with new requirements: he wants your menu to be sorted by name.
This time, before we code the feature, we will write the test first!
# -cut-
describe Food do
# -cut-
it "returns a sorted array of results that match" do
food1 = Food.create(
name: "Nasi Uduk",
description: "Betawi style steamed rice cooked in coconut milk. Delicious!",
price: 10000.0
)
food2 = Food.create(
name: "Kerak Telor",
description: "Betawi traditional spicy omelette made from glutinous rice cooked with egg and served with serundeng.",
price: 8000.0
)
food3 = Food.create(
name: "Nasi Semur Jengkol",
description: "Based on dongfruit, this menu promises a unique and delicious taste with a small hint of bitterness.",
price: 8000.0
)
expect(Food.by_letter("N")).to eq([food3, food1])
end
# -cut-
end
Now when we run "rspec" in our command line, it will return a failure, as expected:
....F
Failures:
1) Food returns a sorted array of results that match
Failure/Error: expect(Food.by_letter("N")).to eq([food3, food1])
NoMethodError:
undefined method 'by_letter' for #<Class:0x007ffa31cd2c50>
# /Users/iqbalfarabi/.rvm/gems/ruby-2.3.1/gems/activerecord-5.0.6/lib/active_record/dynamic_matchers.rb:21:in 'method_missing'
# ./spec/models/food_spec.rb:69:in 'block (2 levels) in <top (required)>'
Finished in 0.40915 seconds (files took 5.89 seconds to load)
5 examples, 1 failure
Failed examples:
rspec ./spec/models/food_spec.rb:50 # Food returns a sorted array of results that match
As previous error message tells us, we will create a class method named "by_letter" in our "Food" model.
class Food < ApplicationRecord
# -cut-
def self.by_letter(letter)
where("name LIKE ?", "#{letter}%").order(:name)
end
end
Now when you execute "rspec" command again, it will return all green. All is well.
# -cut-
describe Food do
# -cut-
it "omits results that do not match" do
food1 = Food.create(
name: "Nasi Uduk",
description: "Betawi style steamed rice cooked in coconut milk. Delicious!",
price: 10000.0
)
food2 = Food.create(
name: "Kerak Telor",
description: "Betawi traditional spicy omelette made from glutinous rice cooked with egg and served with serundeng.",
price: 8000.0
)
food3 = Food.create(
name: "Nasi Semur Jengkol",
description: "Based on dongfruit, this menu promises a unique and delicious taste with a small hint of bitterness.",
price: 8000.0
)
expect(Food.by_letter("N")).not_to include(food2)
end
# -cut-
end
You did good. And good things always end with a git commit.
As you may have noticed that our Food spec is not very faithful to the DRY principle. While we said that we do not really focus on DRY specs, does not mean we forget it altogether.
We can make our specs a little bit more DRY by using "context", "before", and "after".
First, we add more description inside our Food spec:
# -cut-
describe Food do
# -cut-
describe "filter name by letter" do
# Let it be empty for now
end
end
Next, we create a "before" block and put all the test data creation there:
# -cut-
describe Food do
# -cut-
describe "filter name by letter" do
before :each do
@food1 = Food.create(
name: "Nasi Uduk",
description: "Betawi style steamed rice cooked in coconut milk. Delicious!",
price: 10000.0
)
@food2 = Food.create(
name: "Kerak Telor",
description: "Betawi traditional spicy omelette made from glutinous rice cooked with egg and served with serundeng.",
price: 8000.0
)
@food3 = Food.create(
name: "Nasi Semur Jengkol",
description: "Based on dongfruit, this menu promises a unique and delicious taste with a small hint of bitterness.",
price: 8000.0
)
end
end
end
Then we create two contexts, one for matching letters and another one for non-matching letters. Lastly, we put our examples inside the contexts.
# -cut-
describe Food do
# -cut-
describe "filter name by letter" do
# -cut-
# We put the contexts below before block
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
Now if you run "rspec --format documentation" in your console, the result should look like this:
Food
is valid with a name and description
is invalid without a name
is invalid without a description
is invalid with a duplicate name
filter name by letter
with matching letters
returns a sorted array of results that match
with non-matching letters
omits results that do not match
Finished in 0.45187 seconds (files took 6.24 seconds to load)
6 examples, 0 failures
You did good. And good things always end with a git commit.
So far we have learned how to create specs for Model. Here are some important things to remember:
1.a. Specs for price - valid for numeric greater or equal to 0.01
# -cut-
describe Food do
# -cut-
it "is valid with numeric price greater or equal to 0.01" do
food = Food.new(
name: "Nasi Uduk",
description: "Betawi style steamed rice cooked in coconut milk. Delicious!",
price: 0.01
)
expect(food).to be_valid
end
# -cut-
end
1.b. Specs for price - invalid for non numeric price
# -cut-
describe Food do
# -cut-
it "is invalid with non numeric price" do
food = Food.new(
name: "Nasi Uduk",
description: "Betawi style steamed rice cooked in coconut milk. Delicious!",
price: "sepuluh ribu"
)
expect(food.errors[:price]).to include("is not a number")
end
# -cut-
end
1.c. Specs for price - invalid for numeric price less than 0.01
# -cut-
describe Food do
# -cut-
it "is invalid with non numeric price" do
food = Food.new(
name: "Nasi Uduk",
description: "Betawi style steamed rice cooked in coconut milk. Delicious!",
price: -1000
)
expect(food.errors[:price]).to include("must be greater than or equal to 0.01")
end
# -cut-
end
1.d. Specs for image_url - valid for string ending with ".gif", ".jpg", or ".png"
# -cut-
describe Food do
# -cut-
it "is valid with image_url ending with '.gif', '.jpg', or '.png'" do
food = Food.new(
name: "Nasi Uduk",
description: "Betawi style steamed rice cooked in coconut milk. Delicious!",
imgae_url: "Nasi Uduk.jpg",
price: 0.01
)
expect(food).to be_valid
end
# -cut-
end
1.e. Specs for image_url - invalid for string ending not with ".gif", ".jpg", or ".png"
# -cut-
describe Food do
# -cut-
it "is invalid with image_url ending not with '.gif', '.jpg', or '.png'" do
food = Food.new(
name: "Nasi Uduk",
description: "Betawi style steamed rice cooked in coconut milk. Delicious!",
imgae_url: "Nasi Uduk.csv",
price: 0.01
)
expect(food.errors[:image_url]).to include("must be a URL for GIF, JPG or PNG image.")
end
# -cut-
end
2. DRYer specs for invalid without name and description
# -cut-
describe Food do
# -cut-
describe "invalid without name or description" do
before :each do
@food = Food.new(
name: nil,
description: nil,
price: 10000.0
)
end
it "is invalid without name" do
@food.valid?
expect(@food.errors[:name]).to include("can't be blank")
end
it "is invalid without description do
@food.valid?
expect(@food.errors[:description]).to include("can't be blank")
end
end
# -cut-
end