Introduction
In my earlier years of software development, I paid little attention to the practice of building tests for my production applications. Of course my blood pressure, stress, and anxiety levels reached new peaks during the deployment of new code. And my reasons for not testing are I believe common amongst the community. It is thought that "writing tests take too long". I have realized that this feeling could be caused by the absence of structure and an underlying method that drives test/spec creation.
In this article, I share my thought process and methods (/w examples) for creating specs/test for my Ruby & Ruby On Rails projects. My methods are highly influenced by my previous mentors and Better Specs. I hope that you (the reader) after reading this article will be empowered to add test/spec creation in their everyday workflow.
Let's feel good when we deploy new code :)
What are you going to describe
and what is your subject
?
Without using subject
or describe
it can be hard to know exactly what the author of the spec was intending. I utilize describe
in specs to keep track of what part of my code/method I am testing and subject
to define it. Here is an example of a spec that describes some Animal
object and a instance method called #speak
:
describe AnimalSpec do
describe '#speak' do
# Any call to `subject` will execute & return the output of the code block.
# We now know that the author intends to test the output of #speak on varying
# instances of `Animal`
subject { Animal.new.speak }
it "should return an animal noise" do
# The code beneath then does something more like:
# expect(Animal.new.speak).to eq('an animal noise')
expect(subject).to eq('an animal noise')
end
end
end
This keeps things tidy and more importantly focused. That is, we are testing here the output of varying instances of Animal
. Although this test isn't that interesting yet. Let's utilize let
in the next section to enable us to match varying context
and ultimately test the various outputs of your subject
Let's use let
to setup the context
In order to expand our specs to cover varying instances of Animal
and their corresponding output for #speak
, we will utilize let
and context
. The let
method allows us to modify the output value of methods. And the context
is made to describe in written words what situation the subject
is in. Here is an example with the same Animal
spec but this time add variance of the output by introducing the animal_type
and angry
parameter:
describe AnimalSpec do
describe '#speak' do
subject { Animal.new(animal_type: animal_type, angry: angry).speak }
context "when the animal_type is 'duck'" do
# We utilize `let` to change the inputs of our subject to match
# the situation we are describing in our context.
let(:animal_type) { 'duck' }
context "and the animal is not angry" do
# Moreover, you can utilize `let` to continue to describe the
# context further.
let(:angry) { false }
it "should return 'quack'" do
expect(subject).to eq('quack')
end
end
context "and the animal is angry" do
let(:angry) { true }
it "should return 'QUACK!'" do
expect(subject).to eq('QUACK!')
end
end
end
context "when the animal_type is 'dog'" do
let(:animal_type) { 'dog' }
context "and the animal is not angry" do
let(:angry) { false }
it "should return 'woof'" do
expect(subject).to eq('woof')
end
end
context "and the animal is angry" do
let(:angry) { true }
it "should return 'WOOF!'" do
expect(subject).to eq('WOOF!')
end
end
end
end
end
By using let
and context
we can more clearly display our situations and varying characteristics of our Animal
instances.
Applying it to Ruby On Rails projects
Now that we've covered the basics for building specs for plain ruby
code. Let's now dive a little deeper by putting this into practice for some common situations you may encounter when working with Rails.
Model Validation
Let's say you have a User
database model that requires a valid unique email and a password. We can first create a describe
block that says "validations" and a subject the returns the #errors
after performing #valid?
. In this approach, I define in the outermost context the valid attributes for a User and with let
modify them into a invalid state which we can test generates an expected error:
describe User do
describe 'validations' do
# I want to execute the `#valid?` operation to populate the errors and
# then the errors for the following conditions.
subject { user.valid?; user.errors }
let(:user) { User.new(user_attrs) }
# Note - `let` (and `subject`) also accepts code blocks
let(:user_attrs) do
{
email: email,
password: password
}
end
let(:email) { 'fake_valid_email@example.com' }
let(:password) { 'fake_password' }
context 'when the user_attributes are valid' do
# No need to use `let` here as the outermost is already defining
# the valid attributes.
it 'should have no errors' do
expect(subject).to be_empty
end
end
context 'when the email is nil' do
# This overwrites the value in the outer context.
let(:email) { nil }
# Be descriptive about what your are expecting to be returned
# by your `subject`
it "should have an error that says email can't be blank" do
# `subject` is the errors hash as defined above. So we can
# dig in and see what errors are included.
expect(subject[:email]).to include("can't be blank")
end
end
context 'when the email is in an invalid format' do
let(:email) { 'not_a_email' }
it "should have an error that says email is invalid" do
expect(subject[:email]).to include("is invalid")
end
end
context 'when the email has been taken already' do
# To trigger the expected uniqueness error we create User prior to calling
# `subject` with the same email
before do
User.create!(email: email, password: password)
# or also `User.create!(user_attrs)` works
end
it 'should have an error that says has already been taken' do
expect(subject[:email]).to include('has already been taken')
end
end
context 'when the password is nil' do
let(:password) { nil }
it "should have a error that says password can't be blank" do
expect(subject[:password]).to include("can't be blank")
end
end
end
end
This approach gives us a guideline to handling the various error states that a instance of User
could be in. These ideas can be expanded to larger concepts or situations by merely changing the subject
and the let
that configure it into varying states.
Side-Effects Or Queueing Of Jobs
Once in a while you may encounter situations where you don't exactly care about the return value of your subject
but rather the side-effects it has. For example, let's say you have a method on User
called #reset_password
in which you only care that the instance of User
now has their password reset via a change event. Here we utilize the -> { code }
(lambda) syntax to make it more apparent that we are interested in the effects of executing the code rather than the output:
describe User do
describe '#reset_password' do
# The subject is now a lambda which you can trigger via `subject.call`
# It will become more apparent why we want to do this in the next sections.
subject { -> { user.reset_password } }
let(:user) { User.create!(password: original_password) }
let(:original_password) { 'original_password' }
it "should change the password (method 1)" do
# Check that the password is initially set to what you expect
expect(user.password).to eq(original_password)
subject.call
# Use reload to ensure you get the latest copy of the `User` record
expect(user.reload.password).not_to eq(original_password)
end
# Or asserting that the change happened via `change` matcher
it "should change the password (method 2)" do
expect { subject.call }.to change(user.reload.password)
end
end
end
Now let's say we wanted to add some extra functionality on #reset_password
that enables us to specify that password reset instructions should be emailed to that user. Let's make that parameter be send_instructions
and utilize mocking to check that the job or class responsible for sending the email has been called or not called. Let's call that job PasswordResetSender
:
describe User do
describe '#reset_password' do
subject { -> { user.reset_password(send_instructions: send_instructions } }
# Let's mock out the class that would be triggering so we can see that it
# was called were we expect it to be.
before do
allow(PasswordResetSender).to receive(:deliver)
end
context "when send_instructions is set to true" do
let(:send_instructions) { true }
it "should trigger a sending the password reset instructions" do
subject.call
# Check that the class responsible for sending the password instruction
# had been sent.
expect(PasswordResetSender).to have_received(:deliver)
end
end
context "when send_instructions is set to false" do
let(:send_instructions) { false }
it "should not trigger a sending the password reset instructions" do
subject.call
# Vice a versa, we want to ensure sending the password instruction does
# not occur.
expect(PasswordResetSender).not_to have_received(:deliver)
end
end
end
end
Conclusion
In this article, I have covered my thought process & methodology in applying RSpec to your Ruby or Ruby On Rails project. I hope this has been enlightening and inspiring to add test/spec into your regular workflow.
This is just one resource out of many that provides tips on how to apply RSpec effectively. One of my favorite sources where I drew inspiration for ideas is BetterSpecs.
Feel free to reach out to me if you have any questions or other situations that you want to know how to effectively spec out :).
Top comments (0)