Tests are an integral part of most well-working Rails applications where maintenance isn't a nightmare and new features are consistently added, or existing ones are improved. Unfortunately, for many applications, a production environment is where they are put under heavy workload or significant traffic for the first time. This is understandable as such tests are costly.
Thankfully, Rails has good support not only for unit, end-to-end, and integration tests but also for tests related to performance and loading. I’ll cover all of them in the article and show some practical examples that will help you understand how to efficiently use tools that test the performance level of your application.
The article is divided into two sections:
Theoretical — I'll show you why testing is necessary, the kinds of tests we can perform and the metrics that are essential when performing tests on an application
Practical — we'll get our hands dirty and write tests for an actual application to get the output
After reading the two sections, you'll have a deeper understanding of the different types of tests and how to perform them on your Rails application. Sounds interesting? Then let's get started with a pinch of theory about tests.
Testing in Theory
Testing should always be an inherent part of the development of any type of application. If you are still not convinced about that or haven’t written any tests yet, here are some arguments for testing that will help you:
- Introduce changes without worrying about breaking something — this is the major reason why tests are necessary. Imagine working on a huge app where you have to click through the whole app to make sure nothing is broken each time you introduce some change, even a small one. With tests, you just execute one command and the verification process is automatic and fast.
- Easy refactoring process — I mentioned above that tests are essential when adding new features or making changes. With testing in place, you are also more comfortable with improving your existing code.
- Tests are a form of documentation — well-written tests can be a form of documentation for various sets of features in the application. They not only describe what the feature is but also how it should be working.
- Opportunity to rethink the implementation — when you write a test, you have a chance to think again if the way you want to implement the code is correct and reasonable. Also, you simply check if your code is working the way you expect it.
I hope the above arguments convinced you to use tests during the development of any app. While knowing why to test the code is essential, it’s also crucial to learn about different types of tests.
Different Types of Tests
There are three primary types of tests that you can write to ensure that your Rails application’s performance is correct and the infrastructure is working well under the heavy workload. Those types are the following:
- Load testing — this type of test answers the following question: how many simultaneous users can the system handle for a given period. Imagine that you launch a top-rated product on your website and thousands of users want to make the order at the same time. Without proper loading tests, you risk a crash during the most critical time.
- Stress testing — with this type of test, you don’t focus on verifying the number of users the system can handle simultaneously, but on how the system will behave when the limit of the users is hit.
- Performance testing — I would say that this type of test is a parent of stress and load testing. The primary purpose of such tests is to get a specific set of metrics on which base we can take some action to improve the application’s code. I will talk about those metrics in a while.
That being said, we are now prepared to move to the last step of the theory part: learning what metrics are essential when doing performance testing on a Rails application. Without that knowledge, we won’t correctly interpret the test output and decide if we should change the code or not.
Important Metrics
The type of metrics you can receive can be different depending on the tool you use for testing, but generally, we can group them into a set of metrics that are pretty common:
- Response time — the time between the request being made and the response getting rendered in the browser. This metric shows us how long the user needs to wait before receiving the information he requested. It’s sometimes called process time.
- Memory usage — the amount of memory consumed for the given request. This is a piece of essential information as it points you to the place where you can improve the code so the system can respond faster and use fewer resources.
- Objects allocation — a high memory allocation causes high memory usage and long response times. This metric can lead you to the exact place in code where many objects are allocated, so you can immediately inspect that.
You can have more metrics when testing, but those three are the most important and will be valid for any application that you test. We can now get our hands dirty and write real tests.
Practice
We aren't able to write tests without having something to test. That’s why the first step in the practice part is to write a simple Rails application that we can write the tests for.
Sample Rails Application
I will use Ruby 3.0.1 and Rails 6.1.3.1 but feel free to use any version you are comfortable with. If you have Ruby and Rails installed, the next step is to create the application’s skeleton:
1rails new simpleapp -d=postgresql
For the article's purpose, I'll create an app where a list of users is presented along with their pet’s names. Such a structure will allow us to easily create the N+1 queries that will offer more fun when doing performance tests and checking the impact on speed and other metrics that changes will have.
Before we generate the models, let’s create the database first:
1cd simpleapp/
2bin/rails db:create
Now, we can generate the models:
1rails g model User name:string
2rails g model Animal name:string user:references
3bin/rails db:migrate
Just one small update to the User
model to reflect the relationship with the Animal
model:
1class User < ApplicationRecord
2 has_many :animals
3end
We can now add some seeds in db/seeds.rb
file:
1people = {
2 'Tim' => ['Pinky', 'Rick'],
3 'Martha' => ['Rudolph'],
4 'Mark' => ['Niki', 'Miki', 'Bella'],
5 'Tina' => ['Tom', 'Luna']
6}
7
8people.each_pair do |name, pets|
9 user = User.create(name: name)
10 pets.each do |pet_name|
11 user.animals.create(name: pet_name)
12 end
13end
and load the data into the database:
1bin/rails db:seed
I'll create one controller with the users’ assignment, and then in view, I'll list all users with their pets’ names. I’m intentionally using code that is causing performance problems so you can measure the improvements later.
1touch app/controllers/home_controller.rb
2mkdir app/views/home
3touch app/views/home/index.html.erb
The controller is simple:
1class HomeController < ApplicationController
2 def index
3 @users = User.all
4 end
5end
and the view also:
1<h1>List</h1>
2
3<ul>
4 <% @users.each do |user| %>
5 <li><%= user.name %> (<%= user.animals.count %>)
6 <ul>
7 <% user.animals.each do |animal| %>
8 <li><%= animal.name %></li>
9 <% end %>
10 </ul>
11 </li>
12 <% end %>
13</ul>
The last step is to update the config/routes.rb
file to let Rails know what we would like to see when visiting the main URL:
1Rails.application.routes.draw do
2 root to: 'home#index'
3end
Load Tests With JMeter
JMeter is an open-source software created by the Apache software foundation, designed to load test functional behavior. Since it’s a program created with Java, you can install it on any operating system. You can download the files here: https://jmeter.apache.org/download_jmeter.cgi
If you are working on a macOS system, you can easily install JMeter with Homebrew:
1brew install jmeter
After installation, you can run the program with the following command:
1jmeter
Configuring the test
The configuration process consists of the following steps:
- Adding the thread group — specifying the number of users and how long each will visit your website
- Configuring HTTP request — specifying the endpoint that JMeter should hit
- Setting the metrics we are interested in
Let’s walk step-by-step through a simple test configuration to simulate a single user request to the main page of the simple app we created before.
Add thread group
Select the Add -> Threads (Users) -> Thread Group from the menu that expands after you right-click on the “Test Plan”:
Specify the number of users and additional attributes:
Configure HTTP request
Right-click on the thread we created in the previous step and select Add -> Sampler -> HTTP Request:
Configure the protocol, server name, port, and the path of the request:
Specify the result view
Right-click on the HTTP request and select Add -> Listener -> View Results Tree:
Running the Test
The test is now configured, and we can trigger it. To do this, simply click on the green play button:
As you can see, the application passed the test, but it was just a single request, so the result was obvious. You can now experiment with the number of users and other configuration options to see how the application will behave. From my tests, the simple app started to crash when around 200 users started accessing it simultaneously.
Next Steps
After performing the load test, you'll know the pain points of your application. Understanding the user limit, you can now perform the stress test to see how the application will behave.
Performance Tests With Ruby-prof
The performance test feature was built-in in Rails until version 3, and then it was extracted to the separate gem https://github.com/rails/rails-perftest. Since I had some problems using it with the latest version of Rails, I decided not to include it in this article. Instead, I will use the ruby-prof library that works very well.
As usual, the first step is to add a gem to our application:
1bundle add ruby-prof
The second and the last step of the configuration process is to update the config/application.rb
and use the middleware for the gem so the library can automatically inspect our requests and produce reports based on them:
1module Simpleapp
2 class Application < Rails::Application
3 config.middleware.use Rack::RubyProf, :path => './tmp/profile'
4 end
5end
You can now access the app, and each time you perform a request, the gem will generate a new report. It looks like this:
You can find it under the configured path, which is tmp/profile
in our case. The second report is also generated, and it shows the call stack, which is also a pretty helpful metric when debugging performance issues in a Rails application.
It’s important to remember that setting the cache_classes
and cache_template_loading
settings to true
will slow down the application and overwhelm the application metrics as Rails will try to load the required files.
Summary
Testing is an essential part of every development process. Checking if the code behaves as we want it to is as crucial as verifying if our solutions have good performance. Skipping tests leads to serious problems that impact the app’s performance and your users’ trust. Hopefully, testing is not that hard.
In this article, we covered the following important aspects of testing:
- the reason you should test your code
- the different types of performance tests
- the way you can test the performance of your Rails app
I hope that you are now more convinced why it is important to write tests since you know why and how.
If you're interested in monitoring your app’s performance not just locally but also in the production or staging environments, you should also check out AppSignal.
P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!