Dealing with bugs in Rails applications
Bugs are common in the programming world. Every project, even with the development team’s best practices, is susceptible to bugs. In this article, we’ll learn more about bugs and debugging techniques.
A bug
A bug occurs when the software does not behave as expected. It could be an error, a crash, or even the wrong information being output. In 1947, engineers at Harvard University discovered a moth in one of the MARK II components, which caused computer problems. This incident led to the name bug being used to indicate the cause of a problem in computing. Having the ability to identify and resolve bugs is very important. Sometimes, they cause minor inconveniences, but more serious bugs can make a piece of software unusable and unreliable.
Debugging
Debugging is the act of investigating and solving a bug in an application. A simple and basic path to debug could be:
Steps to Debug
- Identify the bug
- Reproduce the bug
- Identify the bug causes
- Fix the bug
- Test the bug to ensure that the fix works
- Creating regression tests
To aid us in debugging, we have access to some tools:
Debugger
A debugger is a tool that helps us inspect code while running. With it, you can create breakpoints that will stop the execution of the code. During this time, you can inspect the application’s current state, such as variables and methods. The debugger also gives us access to watchpoints, making it possible to check the value of a specific variable or pause the code when a specific variable changes. On Rails, we have the gems byebug and pry, and code editors such as JetBrains and VS Code have debuggers integrated into the platform.
REPL’s
REPL (Read-Eval-Print Loop) is a user-interactive interface where you can:
- Read: Insert a command or code
- Eval: The command is executed
- Print: The result is exhibited
- Loop: The process repeats
On Rails, we have the IRB, pry, and rails c. REPLs allow us to execute and explore our application in a safe space. We can call a method with different parameters and analyze the output without having to reproduce the entire application flow.
Client/Browser
You can see how the bug is happening through the client/browser. This is very useful when dealing with a visual bug on the front, for example. Most browsers have tools to help developers debug, such as Chrome DevTools and Firefox Developer Tools.
- API clients such as Postman can also be beneficial when debugging requests.
Log
Logs are a registry of events or messages generated by the application. They are a great tool for tracking bugs. Syslog defines different log levels, but we can stick to the debug level for debugging. Logs are saved in files like log/development.log or log/production.log. We can use commands like Rails.logger.info and Rails.logger.error to include a message using Rails.
Tracking the bug
Reproducing the bug
The most important thing when dealing with a bug is reproducing it. This way, we can see the context and analyze what is causing it. This is why providing context when reporting bugs on applications is very important. You can have two bugs that return the same error to the user but have different causes, so, with context, you can understand and solve that specific case. When the application has a frontend, the tab network on the browser is very useful. There, you can see all the parameters that were sent on the request and what the application returned.
Tracking the application path
When you reproduce your bug, the next step is to see the code’s path. Starting with the route, we can see which controller and method were called. This action also helps us decide where to put our breakpoints when using a debugger.
Regression testing
When dealing with bugs, any modification will change the existing flow of the application. In the long run, without testing, this can lead to two different problems:
- Insertion of a new bug: When fixing a bug, you can modify an existing flow that is not tested on the project and accidentally create a new bug. Since this hypothetical flow is not tested, the bug will pass as unseen.
- Reverting the correction of a bug: If a bug occurs on a specific flow, if after being solved there are no new tests for it, on a future modification, another person can make a change on the code that will bring that bug back, with testing if someone changes the code and make the same bug happens again, it will fail the test suite.
These are two cases that fall under regression tests, but what are regression tests? Regression tests are a type of tests that ensure that the existing features still work well after new features (or changes) are introduced in the code. It will ensure the latest changes don’t cause “regressions” on the previously stable code. Thinking about our previous examples, we can think of regression changes in two ways:
- The first one is about not breaking anything while we solve our bug, and the second one is to ensure that no one in the future will bring our bug back on the application!
- The difference between regression testing and regular testing done during development is that regression testing is focused on testing unplanned side effects that can result from changes while testing on the development of new features is focused on ensuring the correct behavior of the code.
So, when we are solving a bug we must include tests to ensure that at least the code will not revert that bug. We usually do that by creating a setup on our tests that is similar to the context of the bug, and we ensure that the application returns the correct results.
Monitoring
When setting up an application in production, it is also very important to use a tool that monitors bugs and errors. This way, when something happens, it’s easier to have a context for the situation instead of a generic “something is broken on the application.” Rollbar is a well-known tool for getting those logs, but others are available on the market.
Let’s debug one application!
This is the application we will use to test our knowledge. It is a simple application that receives a spreadsheet and creates or updates employee information on our database. The user provided us with the information that they tried to update the data of one employee and received the message “Validation failed: Email has already been taken”. They also sent us the spreadsheet that they were having issues with.
Reproduce the bug
With the provided information, we can guess that the bug occurs according to some conditions when trying to update an employee. Our next step is to reproduce the bug. Based on the spreadsheet that was sent, we’re going to create the employee with the same information and try updating it with the same spreadsheet.
After doing this flow, we got the same error.
So, we successfully reproduced the bug.
Check the request path
Now, we are going to identify the cause. First, we will check the route that this process takes. We can see that it sends a request with the file to the bulk_insert method on the EmployeesController. You can check this by looking at the request that was made on the browser client.
Track the Application Flow
def bulk_insert
if params[:file].blank?
return render json: { error: 'No file provided' }, status: :unprocessable_entity
end
service = InsertEmployeesService.new(params[:file])
result = service.call
if result[:success]
flash[:notice] = 'Employees processed successfully'
else
flash[:alert] = 'Errors occurred while processing employees'
flash[:errors] = result[:errors]
end
redirect_to employees_path
end
end
Based on the message, our flow entered the InsertEmployeesService and returned a failure, so we need to investigate this service.
class InsertEmployeesService
def initialize(file)
@file = file
end
def call
result = { success: true, errors: [] }
begin
parser = ExcelParserService.new(@file)
employee_data = parser.parse
if employee_data.empty?
result[:success] = false
result[:errors] << "The file is empty or has no valid data"
return result
end
upsert_service = EmployeeUpsertService.new(employee_data)
upsert_errors = upsert_service.upsert
if upsert_errors.any?
result[:success] = false
result[:errors].concat(upsert_errors)
end
result
rescue Roo::FileNotFound => e
result[:success] = false
result[:errors] << "File not found: #{e.message}"
result
rescue => e
result[:success] = false
result[:errors] << "Unexpected error: #{e.message}"
result
end
end
end
Now is when the debugging tools mentioned before help us. Since this service calls two other ones, it is hard to see where the bug is unless we look inside the services.
In this situation, one possible approach is to debug after the parser and check if the information is correct. To make this possible, we will use the gem byebug.
class InsertEmployeesService
def initialize(file)
@file = file
end
def call
result = { success: true, errors: [] }
begin
parser = ExcelParserService.new(@file)
employee_data = parser.parse
byebug
if employee_data.empty?
result[:success] = false
result[:errors] << "The file is empty or has no valid data"
return result
end
upsert_service = EmployeeUpsertService.new(employee_data)
upsert_errors = upsert_service.upsert
if upsert_errors.any?
result[:success] = false
result[:errors].concat(upsert_errors)
end
result
rescue Roo::FileNotFound => e
result[:success] = false
result[:errors] << "File not found: #{e.message}"
result
rescue => e
result[:success] = false
result[:errors] << "Unexpected error: #{e.message}"
result
end
end
end
After that, we make the request again with the file, and a console will appear on the terminal running the application. Here, we can interact with all the variables available in that context. If we examine the employee_data, we will find that the information is accurate and matches the data in the spreadsheet that was used. Therefore, the issue must be with the other service.
class EmployeeUpsertService
def initialize(employee_data)
@employee_data = employee_data
end
def upsert
errors = []
@employee_data.each_with_index do |data, index|
begin
upsert_employee(data[0], data[1], data[2])
rescue ActiveRecord::RecordInvalid => e
errors << { line: index + 2, message: "Validation failed: #{e.record.errors.full_messages.join(', ')}" }
rescue => e
errors << { line: index + 2, message: "Unexpected error: #{e.message}" }
end
end
errors
end
private
def upsert_employee(name, email, age)
employee = Employee.find_or_initialize_by(email: email)
employee.assign_attributes(
name: name,
age: age
)
employee.save!
end
end
Taking a closer look at this service we can see that the method being called is the upsert, and it calls the upsert_employee
def upsert_employee(name, email, age)
byebug
employee = Employee.find_or_initialize_by(email: email.downcase)
employee.assign_attributes(
name: name,
age: age
)
employee.save!
end
We can check the parameters on the console. If we search for an employee by email, we see that it returns nil, but we already created an employee with this email. Since the last employee we created on our development database was related to the bug, we can check the email. The email from the spreadsheet is ‘Mark@email.com’, and the one on the database is ‘mark@email.com.’
If we look at the employee model, we’ll see that before creating the entry, the application downcases the email.
class Employee < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true, length: { maximum: 255 }, format: { with: URI::MailTo::EMAIL_REGEXP }, uniqueness: { case_sensitive: false }
validates :age, presence: true, numericality: { only_integer: true, greater_than: 0 }
before_create :downcase_email
private
def downcase_email
self.email = self.email.downcase if self.email.present?
end
end
Solve the bug
So, the application could not find the employee because the search was case-sensitive. In addition, when the entry was created, there was an error in the uniqueness validation of the email. One approach to solving this bug is to downcase the email before searching it.
def upsert_employee(name, email, age)
employee = Employee.find_or_initialize_by(email: email.downcase)
employee.assign_attributes(
name: name,
age: age
)
employee.save!
end
If we rerun the flow, we’ll see that everything is working!
Create regression tests
But we still need to do something essential: Create regression tests.
require 'rails_helper'
RSpec.describe InsertEmployeesService, type: :service do
describe '#call' do
let(:service) { described_class.new(file) }
context 'when the file is valid' do
let(:file) { fixture_file_upload('employees.xlsx', 'application/vnd.ms-excel') }
it 'returns success' do
result = service.call
expect(result[:success]).to be true
expect(result[:errors]).to be_empty
end
it 'creates employees' do
expect { service.call }.to change(Employee, :count).by(1)
end
end
context 'when the file is empty' do
let(:file) { fixture_file_upload('empty.xlsx', 'application/vnd.ms-excel') }
it 'returns an error' do
result = service.call
expect(result[:success]).to be false
expect(result[:errors]).to include("The file is empty or has no valid data")
end
end
context 'when the employee is already created' do
let(:file) { fixture_file_upload('employees.xlsx', 'application/vnd.ms-excel') }
let!(:employee) { create(:employee, email: 'mark@email.com') }
it 'returns success' do
result = service.call
expect(result[:success]).to be true
expect(result[:errors]).to be_empty
end
end
end
end
The idea is also to reproduce the bug on our test suite, so if the codebase changes in the future, this test will break.
Conclusion
Debugging is a huge part of a developer’s life, so it’s important to know the tools that can make your life easier. As with everything in life, the more you do, the better you get. The knowledge of the codebase that you are using also contributes to the difficulty of this task. With time and practice, debugging can become easier, so don’t give up on the bugs that you will face in your journey.
Repository
The project used on the examples can be found here.
References
- https://campuscode.com.br/conteudos/usando-pry-no-dia-a-dia-de-desenvolvimento
- https://katalon.com/resources-center/blog/regression-testing
- https://github.com/resources/articles/software-development/regression-testing-definition-types-and-tools
- https://guides.rubyonrails.org/debugging_rails_applications.html
- https://medium.com/automa%C3%A7%C3%A3o-com-o-byebug-2257ae37c3e
We want to work with you. Check out our "What We Do" section!