Performance, Stress, and Load Tests in Rails

Paweł Paweł Dąbrowski on

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:

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:

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:

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:

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:

1
rails 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:

1
2
cd simpleapp/
bin/rails db:create

Now, we can generate the models:

1
2
3
rails g model User name:string
rails g model Animal name:string user:references
bin/rails db:migrate

Just one small update to the User model to reflect the relationship with the Animal model:

1
2
3
class User < ApplicationRecord
  has_many :animals
end

We can now add some seeds in db/seeds.rb file:

1
2
3
4
5
6
7
8
9
10
11
12
13
people = {
  'Tim' => ['Pinky', 'Rick'],
  'Martha' => ['Rudolph'],
  'Mark' => ['Niki', 'Miki', 'Bella'],
  'Tina' => ['Tom', 'Luna']
}

people.each_pair do |name, pets|
  user = User.create(name: name)
  pets.each do |pet_name|
    user.animals.create(name: pet_name)
  end
end

and load the data into the database:

1
bin/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.

1
2
3
touch app/controllers/home_controller.rb
mkdir app/views/home
touch app/views/home/index.html.erb

The controller is simple:

1
2
3
4
5
class HomeController < ApplicationController
  def index
    @users = User.all
  end
end

and the view also:

1
2
3
4
5
6
7
8
9
10
11
12
13
<h1>List</h1>

<ul>
  <% @users.each do |user| %>
    <li><%= user.name %> (<%= user.animals.count %>)
      <ul>
        <% user.animals.each do |animal| %>
          <li><%= animal.name %></li>
        <% end %>
      </ul>
    </li>
  <% end %>
</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:

1
2
3
Rails.application.routes.draw do
  root to: 'home#index'
end

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:

1
brew install jmeter

After installation, you can run the program with the following command:

1
jmeter

Configuring the test

The configuration process consists of the following steps:

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”:

alt text

Specify the number of users and additional attributes:

alt text

Configure HTTP request

Right-click on the thread we created in the previous step and select Add -> Sampler -> HTTP Request:

alt text

Configure the protocol, server name, port, and the path of the request:

alt text

Specify the result view

Right-click on the HTTP request and select Add -> Listener -> View Results Tree:

alt text

Running the Test

The test is now configured, and we can trigger it. To do this, simply click on the green play button:

alt text

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:

1
bundle 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:

1
2
3
4
5
module Simpleapp
  class Application < Rails::Application
    config.middleware.use Rack::RubyProf, :path => './tmp/profile'
  end
end

You can now access the app, and each time you perform a request, the gem will generate a new report. It looks like this:

alt text

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:

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!

Our guest author Paweł is an open-source fan and growth seeker with over a decade of experience in writing for both human beings and computers. He connects the dots to create high-quality software, valuable relations with people and businesses.

5 favorite Ruby articles

10 latest Ruby articles

Go back
Ruby magic icon

Subscribe to

Ruby Magic

Magicians never share their secrets. But we do. Sign up for our Ruby Magic email series and receive deep insights about garbage collection, memory allocation, concurrency and much more.

We'd like to set cookies, read why.