From my earlier article we know that the problem with Ruby performance is mostly due to the code that we write. But before we start refactoring it, we have to be sure that our changes will not break our application. To do that, we will need a good test. The problem is that often our tests are large and hard to manage, not to mention whether we are sure if they test all options. To help you write a better RSpec test, in this article I will present some good practices.
# app/models/user.rb
class User {
validates :first_name, presence: true, length: 5..10
validates :last_name, presence: true, length: 5..10
}
# spec/models/user_spec.rb
describe User do
it “should be invalid” do
user = User.new(first_name: ‘name’)
expect(user.valid?).to_not eq true
user = User.new(first_name: to ‘long name to be validated’)
expect(user.valid?).to_not eq true
end
end
Our test is passed and we’re happy. But there is a small problem. This test gives a false positive because even if we pass the validation of ‘first name’, it will be not valid because of the validation of ‘last name’.
Now that we know that our test is wrong, let’s think about what we can do. First, let’s look at the description: ‘should be invalid’. When we see that description, we don’t know what should be invalid. Whether you read or run the test, it should specify how objects should behave.
The second element in this test is the expect:
expect(user.valid?).to_not eq true
When we write a test expectation, we should always finish with “true” and not with “not true”. So let’s refactor this test.
# spec/models/user_spec.rb
describe User do
subject(:user) { described_class.new(first_name: ‘first name’, last_name: ‘last name’) }
end
The main step is to create a valid subject that we will use for the whole test. Thanks to the new line, you have access to 2 variables: user and subject. When you start reading the test, you will always know what specific element in the file you are testing.
Without this new line RSpec would create only a subject variable containing an empty object. The described_class is an RSpec build function that refers to Object that is used in describe; in our example, it is User.
Next, let’s change how to expect elements look like. In the first one, we test 2 things. When one element passes and the second doesn’t, the whole test will fail. In such a case, we still don’t know in the end where the problem is.
# spec/models/user_spec.rb
describe User do
subject(:user) { described_class.new(first_name: ‘first name’, last_name: ‘last name’) }
context ‘with a first_name that is over 10 chars’ do
end
context ‘with a first_name that is under 5 chars’ do
end
end
This refactor step adds 2 new contexts. The first one shows us what will happen when validation fails because the first name is too long and the second one if the first name is too short.
# spec/models/user_spec.rb
describe User do
subject(:user) { described_class.new(first_name: first_name, last_name: ‘last name’) }
let(:first_name) { ‘Przemek’ }
context ‘with a first_name that is over 10 chars’ do
let(:first_name) { ‘long name to be validated’ }
end
context ‘with a first_name that is under 5 chars’ do
let(:first_name) { ‘name’ }
end
end
As I mentioned at the beginning of this part, we should only change one element and for us, this one element is the value of first_name. To always see better which element is changing, we extract it to let.
We have almost refactored all the test code. But the last element is the part of expect that checks true but it is, in fact, false and written as not true.
# spec/models/user_spec.rb
describe User do
subject(:user) { described_class.new(first_name: first_name, last_name: ‘last name’) }
let(:first_name) { ‘Przemek’ }
context ‘with a first_name that is over 10 chars’ do
let(:first_name) { ‘long name to be validated’ }
specify { expect(user).to be_invalid }
end
context ‘with a first_name that is under 5 chars’ do
let(:first_name) { ‘name’ }
specify { expect(user).to be_invalid }
end
end
After all that refactoring, we finish with a readable test that you can easily understand and which tests in every example if something is really true or not.
But this is not the end of the refactoring. One last change that I found very important for testing is to check if an MVO is really an MVO. Without it, if we change validation in the future, we will see that the whole test fails but we will not know whether this is because of the subject validation or other tests.
# spec/models/user_spec.rb
describe User do
subject(:user) { described_class.new(first_name: first_name, last_name: ‘last name’) }
let(:first_name) { ‘Przemek’ }
specify { expect(user).to be_valid }
context ‘with a first_name that is over 10 chars’ do
let(:first_name) { ‘long name to be validated’ }
specify { expect(user).to be_invalid }
end
context ‘with a first_name that is under 5 chars’ do
let(:first_name) { ‘name’ }
specify { expect(user).to be_invalid }
end
end
I hope that you can now see the pattern for writing an MVO.
The simplest way is to:
- Prepare the subject
- Set config elements by let to change them in future
- Describe the test context
- Assert a valid state
In the next part I will focus more on data_set as a list of elements that we are testing. So hold on tight and wait for the next bit, if you got interested. Will be here soon!
Author: Przemek Olesiński