So, you've built an application using Ruby on Rails. Perhaps it started as a pet project, or a learning experience, and testing was put on the backburner. Fast forward to today, and you're left with an application that serves its purpose but lacks tests. Don't worry, you're not alone in this. Many developers, especially when new to Rails, overlook testing in the early stages of development.
This has happened to me several times when I am hired to work on a programming project or a codebase that was built ages ago. Often it was something whipped up quickly just to get out the door, but the programmers didn't plan to stick around long enough to bother with testing. It's not recommend, but it happens. Why preach about it, when you can fix it instead.
Why Test At All
Before we dive into the how, let's focus on the why. Tests serve many purposes. They ensure the correctness of your code, act as a safety net against regressions, aid in refactoring, and can even guide your application's design process following the Test-Driven Development (TDD) approach.
Testing in Rails is usually categorized into three types: Unit, Integration, and System tests. Unit tests target small parts of your code, like model methods or controller actions. Integration tests focus on the interaction between different parts of your application, and System tests simulate user interaction with your app.
What if you've never tested?
If you've never written a test in your life, you likely have a folder called /test/ in your Rails app that is just sitting there. These tests are automatically generated if you use Rail's scaffold generators. However it's possible that your codebase has diverged so much from the default, that starting with this folder will be impossible.
Yes - it's okay to delete your test folder if you've never written any tests here. You can delete it and start over, instead hand-writing tests for every model, controller, and integration piece needed.
In fact, if you're going to use RSpec, you're going to instead use a folder called "spec" and not a folder called "test".
Starting with RSpec
First off, we need a testing framework. RSpec is a commonly used framework in Ruby due to its readability and flexibility. You can compare it to Rails' default, MiniTest. RSpec has more stars and more forks than MiniTest, and I would estimate about 25% more people use RSpec compared to MiniTest. To add it to your Rails app, include RSpec in your Gemfile and run bundle install.
You might also need tools like FactoryBot for creating test data, and Capybara for simulating user interaction. These can also be added via the Gemfile. But you don't really have to start with them immediately.
Lastly, remember to keep your testing environment isolated from development and production. Rails does this automatically by using different databases for each environment. Double check your database configuration file.
Starting Testing
Once set up, start by writing simple tests. For instance, you can test model validations or controller actions. The process usually follows a cycle of Red (write a failing test), Green (make it pass), and Refactor (clean up the code).
It's essential to run your tests regularly. This ensures that your code fails fast, catching bugs before they become problems.
You can create some very basic tests. You can choose just one model to test first. Let's start by testing the simplest possible scenario: creation of a record in the database. For this example, let's assume you have a User model.
--
require 'rails_helper'
RSpec.describe User, type: :model do
it "can be created with valid attributes" do
user = User.new(email: 'test@example.com', password: 'password')
expect(user).to be_valid
end
end
--
In this test, we're creating a new User instance with some attributes (modify this to match your User model's required attributes) and then checking that it's valid. If your User model has any validations, this test will pass if those validations are satisfied, and fail otherwise.
To run this test, use the rspec command in your terminal, like so:
bundle exec rspec spec/models/user_spec.rb
If everything is set up correctly, RSpec will run your test and provide a report indicating whether the test passed or failed. If the test passed, congratulations! You've successfully added your first test to your existing Rails app. If the test failed, the report should give you some indication of what went wrong, which can guide you in troubleshooting the issue.
Remember, the goal here is to incrementally build up your test suite. Start with simple tests like this one, and gradually add more complex tests as you become more comfortable with testing.
Adding Tests to Models
Identify critical parts of your app. Start writing tests for these, focusing on validations, scopes, and methods. Make sure to handle associations correctly and be mindful of dependencies between your models.
Adding Tests to Controllers
Controller testing checks the correct routing, HTTP verbs, actions, rendering, and redirects. It verifies the correct instance variables are set and checks for changes in the model state.
Adding Integration and System Tests
Integration tests are crucial in ensuring different parts of your app work in harmony. System tests, conducted using Capybara, can simulate user flows like account creation, login, and purchases. These are often some of the most important tests, because they help ensure that all the different pieces of your app work well together.
Handling Legacy Systems
In older systems, you may encounter pain points like an out-of-sync database schema or complex, outdated code. Having a well-thought-out testing strategy can help you refactor your code and bring your schema up to date.
Sometimes you'll encounter a legacy project where the state of the database doesn't match what's described in the migrations. This can happen when changes have been made directly to the database, bypassing Rails' migration system.
One solution to this problem is to generate a new set of migrations based on the current state of the database. Rails provides a task for this: rails db:schema:dump. This task will create a db/schema.rb file that describes the current state of the database.
You can then delete your old migrations and create a new migration that loads the contents of db/schema.rb:
--
class InitialSchema < ActiveRecord::Migration[6.0]
def up
load Rails.root.join('db', 'schema.rb')
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
--
This isn't a best practice, but it gives you a place to start and build off of. This will bring your migrations back into sync with the database, but bear in mind that this technique will lose the history of how your database schema evolved over time.
Testing Best Practices
Write clean, readable tests. Test edge and corner cases, and focus on high-risk areas. Don't aim for 100% coverage initially. As you continue developing your app, incrementally add tests.
Writing tests for complex and outdated code can be challenging, but it's not an insurmountable problem. Here are some tips to make the process easier:
Start Small: Start by writing tests for small, isolated parts of the system. As you become more comfortable, you can gradually tackle more complex areas.
Use Factories: Use a factory library like FactoryBot to create objects for your tests. Factories can simplify your tests and make them more robust.
Don't be Afraid to Refactor: If you find that the code is too complex to test easily, it may be a sign that it needs refactoring. Make sure to take small steps, use your judgement, and refactor cautiously. As you add more tests, you'll gain the confidence to refactor larger parts of the system.
Seek Help: If you're struggling to understand how a piece of code works, don't hesitate to ask for help. Other developers, either in your team or in the broader Rails community, can provide valuable insights.
Remember, testing is an incremental process. Don't expect to have a comprehensive test suite overnight. But with patience and consistent effort, you'll start to see the benefits of testing in your legacy Rails app.
Adding tests to an existing Rails application can feel daunting. But with a systematic approach and a focus on high-impact areas, it can be done. Start small, and as you see the benefits of testing firsthand, you'll find yourself writing tests as an integral part of your development process. Happy testing!