Note: This article is a sample chapter in my upcoming book Building a SaaS Ruby on Rails 5. The book will guide you from humble beginnings through deploying an app to production. If you find this type of content valuable, the book is on pre-sale right now!
Testing, while requiring a little bit more cognitive load up front, and even loathed by some developers, will often pay dividends down the road. It will almost always have you saying "I'm glad I wrote tests for this" at some point. TDD(Test Driven Development) is one approach that has grown on the Rails community. Although, not completely accepted by all in the community. This approach has you writing your test(s) first and then writing code to make your tests pass.
Why Test
Testing will provide a few main benefits when done correctly. First, it will help you develop the behaviors within your app. You may write a test that your model is validating the presence of an email. Then realizing you needed to include validations for the password or add a "retype password" input box.
Another great reason to test is to protect yourself against regressions within your application as your modifying code or adding new features. This will also give you confidence and assurance when your refactoring classes, collapsing code or removing extraneous code. If written tests pass both before and after changes you should be good to go! Another testing method is to write a test after you find an issue or bug in your application. Ensuring the fix works and does not plague your application in the future.
Testing Frameworks
Like many other parts of Rails, there are default gems and libraries. As well as other optional alternatives, which at times are more used by than the defaults.
RSpec vs MiniTest
As of the latest version of Rails, the default testing framework that is included is MiniTest. Which in itself is an improvement on the original testing framework for Rails Test::Unit. Personal preference can play a huge part in choosing many aspects of a Rails application. Testing is no exception.
The largest alternative to MiniTest is RSpec. Which focuses on readability and composability of tests. It uses words such as "describes" and "it" when naming tests and test groups. For example:
describe "the test" do
it "is a success" do
expect(1).to eq(1)
end
end
Whereas MiniTest will use the asset verbiage found in other programmatic testing frameworks. Personally, I like RSpec, find it easier to reason about, and has a little bit better of a command line interface(CLI).
Capybara Feature Tests vs Integration Tests
Another noteworthy alternative in the testing work is the addition of Capybara. While you can use built in Integration tests to test workflow. It leaves desiring more for real world browser workflows. This is where Capybara comes in. Allowing you to write feature tests taking you through pages of your choosing within your application. Performing behaviors such as clicking, filling in a form or verifying that everything is behaving as expected.
Capybara plugs right into RSpec, allowing you to use the same syntax and phrasing to write tests. Here is an example right from their docs!
describe "the signin process", :type => :feature do
before :each do
User.make(email: 'user@example.com', password: 'password')
end
it "signs me in" do
visit '/sessions/new'
within("#session") do
fill_in 'Email', with: 'user@example.com'
fill_in 'Password', with: 'password'
end
click_button 'Sign in'
expect(page).to have_content 'Success'
end
end
FactoryGirl
The last alternative I'll mention is the gem FactoryGirl. The default method of test objects within Rails is fixtures. Fixtures files are in written the YAML format. Providing a basic static mock object representation. Fixtures will get tougher as the model objects get more complex. Including their associations proliferating. FactoryGirl describes itself as an API to create test objects. Meaning, you will programmatically use the API to create mock objects for your tests. You can provide a factory definition, in Ruby, then use the API much like normal object to .build
or .create
. FactoryGirl also makes it super simple to include associations through the factory definitions.
Testing Types
There are three main types of tests you will find in a typical Rails application. Model(Unit) test, Controller tests, and View tests. Additionally, you can also use Feature Tests. These tests will function as an end to end walkthrough test of your application. Each type of test has it's place in the testing world and may be equally important.
Model/Unit Tests
Model specs are most likely the simplest of the test types you will deal with in Rails. With these tests you are testing the normal CRUD(Create/Read/Update/Delete) operations on the model object. Additionally, they will test validations and/or associations. Model specs will normally run super quick and be fairly self-contained
FactoryGirl.define do
factory :user do
name "Same Name"
email "test@user.com"
end
end
class User < ApplicationRecord
validates :email, presence: true
validates :email, uniqueness: true
validates :name, presence: true
…
describe User do
it "has a valid factory" do
expect(FactoryGirl.build(:user).save).to be_true
end
it "is invalid without a name" do
expect(FactoryGirl.build(:user, name: nil).save).to be_false
end
it "is invalid without a unique name" do
user = FactoryGirl.create(:user)
expect(FactoryGirl.build(:user, name: "Same Name")).to be_false
end
it "is invalid without an email" do
expect(FactoryGirl.build(:user, email: nil)).to be_false
end
end
For this example, there is an already valid user factory definition that is setting some default values. This particular scenario is testing that the User model can create a user. Then validates user creation fails on a few key Rails Model validators on this User model.
Controller Tests
Controller tests will be your bread and butter when it comes to testing all your conditional logic. With tests based on request type, user type, success/failure of Models and so on. Controller specs are a little bit slower than Model specs due to their use of the request/response cycle. Though are still considerably faster that feature specs. This makes it a great place to do the bulk of your testing.
describe UsersController do
describe "GET index" do
it "assigns @users" do
user = FactoryGirl.create(:user)
get :index
expect(assigns(:users)).to eq([user])
end
it "renders the index template" do
get :index
expect(response).to render_template("index")
end
end
"GET #show" do
it "renders the #show view" do
get :show, {id:user.id}
response.should render_template :show
end
end
end
In this test file, you will find that we are testing a page that would list the users. First, testing the action assigns the right output. After creating a user, and getting the :index action the controller will have an instance variable that equals an array of the one user from the test. The second test is simply testing that getting the :index action, renders the index template.
Obviously, these tests are not as complex and are abbreviated to give you a taste of how easy controller testing can be. In a real world controller file, you would be testing every action. Testing its behavior and rendering. As well as each branch of the action such as success or failure of a database transaction.
View Tests
View specs are often overlooked by developers who test all aspects of the visual representation in feature tests. Feature tests are meant to give you the high-level behavior testing. However, they definitely should not be used to test every last detail of a server rendered template. Many times you will need to test different markup based on some sort of if statement or logic, that can be done in a view spec.
FactoryGirl.define do
factory :user do
name "Same Name"
email "test@user.com"
admin false
end
end
<%- if @user.admin? >
<h1>Welcome back admin</h1>
<%- else %>
<h1>Welcome back pal</h1><
<%- end %>
describe "rendering homepage" do
it "displays admin message" do
assign(:user, FactoryGirl.create(:user, admin: true))
render :template => "home/index.html.erb"
expect(rendered).to match /admin/
end
it "displays regular user message" do
assign(:user, FactoryGirl.create(:user))
render :template => "home/index.html.erb"
expect(rendered).to match /pal/
end
end
In this example, we have a simple view template that renders a message in the markup. The noun is based on whether the user referenced in the user variable is an admin or not. Here, the view spec renders out the message with the user object created in each test(one admin, one not) and tests for the text with regular expressions.
Feature Tests
Finally, feature specs(tests) are a great way to test your entire application at once(or large parts of behavior). Using RSpec and Capybara, you will be able to write plain simple tests that will walk through your app as if you were an end user.
describe "Signing Up", :type => :feature do
it "allows a user to sign up" do
visit root_path
expect(page).to have_content 'Sign Up'
within("form#user_new") do
fill_in 'Name', with: 'John'
fill_in 'Email', with: 'user@example.com'
fill_in 'Password', with: 'password'
fill_in 'Retype Password', with: 'password'
end
click_button 'Sign Up'
expect(page).to have_content 'Welcome to Sample App'<
expect(page).to have_content 'John'
end
end
This test simulates a user visiting your application for the first time. Then seeing a sign-up page, filling it out and successfully accessing the application after signup.
As you can see, Feature tests provide an amazing way to test at a high level, just as a user would be interacting with your application. However, with that comes the cost of the fact feature tests will run slower than all other types of tests. For that reason, it would be smart to not test every branch or possibility in your application. Using controller or other specs to test edge cases.
Testing Tools
Additionally, there a few extra tools I would like to go over and will be included in your application's codebase. These tools will help with issues such as test cleanup. Another, being a test coverage completeness tool which includes a local web tool to see analysis file by file. Lastly, is a tool to easily mock external resources, such as third-party APIs.
Database Cleaner: Database Cleaner is a testing tool, that allows you to use different strategies to wipe the database to a clean state after every test. The importance here is that while Rails attempts to wrap their tests in transactions. Which prevent one tests database changes from affecting another's tests. Yet, when using javascript drivers, those tests are performed in another thread. Meaning you will need another means to return the database to a pristine state, using truncation. Database Cleaner, in particular, will allow you to set the "cleaning" method based on test type.
Simplecov: We will use this gem to calculate, display and investigate code coverage. This tool will use built in Ruby coverage information, along with merging the results of different tools' and test types coverage. Afterward, it will generate the information, save it to a report and automatically create an HTML site for you to visit to look through the code coverage analysis.
VCR: VCR allows you to "record" HTTP transactions and save their responses locally. This allows you to "playback" the response from a file, making it easy and fast to run tests that interact with external resources. Along with the speed increases, it allows for offline testing and not worrying about a third party change affecting your tests. However, I recommend you still perform some sort of testing on the actual external resource's response.
CI and You
The last thing to discuss in this chapter is Continuous Integration. Which means you will be continuously merging changes into a master branch, where various pipelines make work on the newly merged code. In most cases, this is a way to merge changes into one central branch. Then run the tests on an external system and using the test results to chose another action such as deploying or requesting a fix to the codebase.
There are a few different tools one could use for CI. The first that may be recommended in Jenkins. The great thing about Jenkins is that it is an open-source project and free. However, there is no hosted solution available through their site. Meaning, you will either have to build and maintain your Jenkins server or seek a service that would do so for you.
There is good news for those who do not want to maintain their own server for running continuous integration. There are quite a few services that run a CI service. Having integrations wall of the major repository services(GitHub, Bitbucket, etc). This means a simple push to your repository could trigger your test to run automatically on their service. Followed by a report with the results in any response medium you would like. Such as email, chat services, and others.
Without any affiliation, the server I usually use for my applications is CircleCI. I find their ease of use as well as their pricing(free for most of my purposes) to for my needs well. Other choices would be TravisCI and SemaphoreCI. I would recommend checking these products out and choosing the one that fits you best.
Top comments (0)