Rails API with Active Model Serializers – Part 2

In the previous installment of this series, I covered how to build a solid API using Rails 5. Today, I want to show how we should test our API and how to write clean and readable tests using RSpec.

Tests are a really important part of our application. Without them, it’s hard to say which part of the application works, refactor some code or even add a new feature without breaking existing functions. Moreover, good specs are like a system documentation; they show how each part of the application or method should behave.

RSpec setup

We’ll use RSpec as a test framework instead of MiniTest. Please add the RSpec, FactoryGirl and ShouldaMatchers gems to the Gemfile:

RSpec is a test framework for Ruby and Rails applications. It’s really easy to write specs using it. Moreover, if you want test the front-end part of your application, just add the Capybara gem – it painlessly integrates with RSpec!

FactoryGirl is used to create factories for our tests, basically to build some records.

ShouldMatchers helps to test associations, validations and some controller methods.

Now when you know what each part of a system does, let’s install RSpec and initialize it:

$ rails generate rspec:install

And run it to check if everything works ok!

$ rspec

Gems setup

In this case adding gems to the Gemfile isn’t enough. We need to add a small configuration to the application. Please add to this line rails_helper.rb, which enables FactoryGirl:

Also we need to enable pundit test methods. Add pundit to rails_helper.rb (top of the file):

require 'pundit/rspec'

Add factories

Now that we have written some specs, let’s prepare all needed the factories. We need admin, user, author, book and book_copy factories. Create a folder called factories under specs and add the first factory – admin.

As you can see it’s really simple. We just define all needed attributes with their values. As you can see, to define an email, I used sequence. What is it? In each created factory, it fills a record with a sequence value. Why do we need it? Because an email address must be unique – and it is unique, thanks to sequence.

Model specs

Everything is ready to write the first spec – let’s do it. We need to test all the associations, validations and methods in the Author class. Create an author_spec.rb file under the specs/models:

require 'rails_helper'
describe Author do
subject { create(:author) }
describe 'associations' do
it { should have_many(:books) }
end
describe 'validations' do
it { should validate_presence_of(:first_name) }
it { should validate_presence_of(:last_name) }
end
end

What are we testing here? We will check if all validations and associations are present. If you remove one, tests will fail (comment out associations):

1) Author associations should have many books
Failure/Error: it { should have_many(:books) }
Expected Author to have a has_many association called books (no association called books)
# ./spec/models/author_spec.rb:7:in `block (3 levels) in <top (required)>'

Now let’s test the BookCopy class. Testing associations and validations isn’t enough. We have 2 more methods there – borrow and return book. All of these methods should be tested in two ways – successful and unsuccessful scenarios. Let’s write the BookCopySpec:

require 'rails_helper'
describe BookCopy do
let(:user) { create(:user) }
let(:book_copy) { create(:book_copy) }
describe 'associations' do
subject { book_copy }
it { should belong_to(:book) }
it { should belong_to(:user) }
end
describe 'validations' do
subject { book_copy }
it { should validate_presence_of(:isbn) }
it { should validate_presence_of(:published) }
it { should validate_presence_of(:format) }
it { should validate_presence_of(:book) }
end
describe '#borrow' do
context 'book is not borrowed' do
subject { book_copy.borrow(user) }
it { is_expected.to be_truthy }
end
context 'book is borrowed' do
before { book_copy.update_column(:user_id, user.id) }
subject { book_copy.borrow(user) }
it { is_expected.to be_falsy }
end
end
describe '#return_book' do
context 'book is borrowed' do
before { book_copy.update_column(:user_id, user.id) }
subject { book_copy.return_book(user) }
it { is_expected.to be_truthy }
end
context 'book is not borrowed' do
subject { book_copy.return_book(user) }
it { is_expected.to be_falsy }
end
end
end

As you maybe noticed, I like to use one-liners. They’re really clear and readable to me. Moreover, I have a rule that I first declare variables using let. I call the before/after block later, and at the bottom I declare a subject. It really helps me to maintain, organize and read my code.

BookSpec looks really similar to BookCopySpec but there, we need to test the static-class method:

require 'rails_helper'
describe Book do
let(:book) { create(:book) }
describe 'associations' do
subject { book }
it { should have_many(:book_copies) }
it { should belong_to(:author) }
end
describe 'validations' do
subject { book }
it { should validate_presence_of(:title) }
it { should validate_presence_of(:author) }
end
describe '.per_page' do
subject { described_class.per_page }
it { is_expected.to eq(20) }
end
end

Something new is in the UserSpec. Here we need to test the before_save callback. How? Well, we should check to see if a method has been called – using expect(instance).to receive(:method_name).

Another thing to test is if a method does what it should do. I check what happens before and after saving an instance:

require 'rails_helper'
describe User do
let(:user) { create(:user) }
describe 'associations' do
subject { user }
it { should have_many(:book_copies) }
end
describe 'validations' do
subject { user }
it { should validate_presence_of(:first_name) }
it { should validate_presence_of(:last_name) }
it { should validate_presence_of(:email) }
end
describe '#generate_api_key' do
let(:user) { build(:user) }
it 'is called before save' do
expect(user).to receive(:generate_api_key)
user.save
end
it 'generates random api key' do
expect(user.api_key).to be_nil
user.save
expect(user.api_key).not_to be_nil
expect(user.api_key.length).to eq(40)
end
end
end

Controller’s specs

Most of the important specs are controller’s specs. We must test if our endpoints work properly. Also, if we want to modify any method or refactor it, without specs it’s really hard to do.

Specs also have another responsibility, they show how each endpoint should behave. Moreover, in a case when each endpoint does something different for each user role, we should test all the possible cases. We don’t want to give an access to sensitive data for not-permitted users. Let’s start with writing specs for AuthorsController.

Let’s start at the index method. What should we test for here? If it’s accessible only for admins and to see if it returns a valid JSON with records. To pass HTTP Token, you can add to a before block:

BookCopiesController, BooksController and UsersController look almost the same as the AuthorsController so the specs are also almost the same. I won’t cover them, but below you can find the full files.

Now, I want to focus on two methods which are accessible to users and admins – borrow and return_book in the BookCopiesController.

There are a lot of cases; first let’s analyze the borrow method. A book can be borrowed by an admin, when he passes a user_id parameter. Without it, he can’t borrow. Moreover, a book copy cannot be borrowed if it’s already borrowed. As a user, we can borrow a book if it’s not borrowed – really simple.

Now the return_book method. An admin can return a book only with a user_id parameter. Moreover, user_id doesn’t need to match to a book copy’s user_id – an admin is an admin 🙂 and a not borrowed book cannot be returned.

A user can only return a borrowed book by himself, he cannot return a book that doesn’t belong to him.

PolicySpecs

We should also test our policies. They’re a really important part of the application. Right now, in the application we only have one policy. Let’s write some specs – please add the book_copy_policy_spec.rb under the specs/policies folder.

require 'rails_helper'
describe BookCopyPolicy do
let(:user) { create(:user) }
subject { described_class }
permissions :return_book? do
context 'as admin' do
it 'grants access if user is an admin' do
expect(subject).to permit(Contexts::UserContext.new(nil, User.new(admin: true)), BookCopy.new)
end
end
context 'as user' do
it 'denies access if book_copy is not borrowed' do
expect(subject).not_to permit(Contexts::UserContext.new(User.new, nil), BookCopy.new)
end
it 'grants access if book_copy is borrowed by a user' do
expect(subject).to permit(Contexts::UserContext.new(user, nil), BookCopy.new(user: user))
end
end
end
end

As you can see, we tested the return_book? method for an admin and a user. Each case should be covered. Pundit provides some useful methods for RSpec to make testing really simple.

Test coverage

One of the important things when we talk about testing is test coverage. It shows the amount of lines of code covered by tests. Also it shows which file has the least test coverage and which lines should be covered. SimpleCov is a great gem which can build a detailed report about our tests and show a test coverage rate.

Please add to the Gemfile (test group) and install:

gem 'simplecov', require: false

$ bundle install

To make it work, add at the top of the rails_helper.rb:

require 'simplecov'
SimpleCov.start

Now, when you run your tests, SimpleCov will prepare a report.

$ rspec

As you can see, our tests cover the code by 96.5% – which is a great result! Moreover, when you open the coverage/index.html file, it shows a detailed report, which covers how each file has been covered by specs.

Conclusion

In this part of the series, I covered how to write readable and clear specs for your API. You should keep in mind that testing is a really important part of your application. Every good application should include specs which say how each part of the application should behave.

I hope that you liked this series and find it useful! The source code can be found here.

If you want to stay up to date with our articles, please subscribe to our newsletter. Feel free to add your comments and thoughts below!

Related Posts

As you've probably guessed by the title of my article, I still consider Ruby on Rails as a relevant technology that offers a lot of value, especially when combined with ReactJS as it's frontend counterpart. Here's how I approach the topic.

Gitlab Pipeline for Rails is the main part of a powerful GitLab CI/CD tool and can be a useful alternative for other applications like Jenkins and TeamCity. If you’re looking for some more detailed information on exactly how it works, we’ve compiled an example configuration that can help you.