ruby

Performance, Stress, and Load Tests in Rails

Paweł Dąbrowski

Paweł Dąbrowski on

Performance, Stress, and Load Tests in Rails

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:

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

shell
cd simpleapp/ bin/rails db:create

Now, we can generate the models:

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

ruby
class User < ApplicationRecord has_many :animals end

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

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

shell
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.

shell
touch app/controllers/home_controller.rb mkdir app/views/home touch app/views/home/index.html.erb

The controller is simple:

ruby
class HomeController < ApplicationController def index @users = User.all end end

and the view also:

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

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

shell
brew install jmeter

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

shell
jmeter

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

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:

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

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

  • 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!

Paweł Dąbrowski

Paweł Dąbrowski

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 and build valuable relations with people and businesses.

All articles by Paweł Dąbrowski

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps