There are two main testing gems within rails, Rspec and Minitest. Since Minitest ships with Rails, lets focus on how to use it with rails.
If we are starting from scratch, we can create a new app by rails new TestSuite -T
. The -T tells rails not to include any test units upon creating the app. This means that rails will not generate and test or spec files upon generating any scaffolds or models.
We can add minitest to our project by opening vim gemfile
and adding the following code:
group :test do
gem 'minitest'
end
This lets us use it in our testing environment. Now run bundle install
.
Now let's create a new folder for our test and a new helper config file mkdir test && touch minitest_helper.rb
.
Open up the newly created file vim minitest_helper.rb
. Paste the following code into it:
ENV["RAILS_ENV"] = "test" # Forces it to be in test environment
require File.expand_path("../../config/environment", __FILE__) # loads the application
require "minitest/autorun"
To create a model test, cd test && mkdir models
, then create your model file vim user_test.rb
and add these line at the very top. This will load our configuration we created earlier before running the future test.
require "minitest_helper"
There are two ways to use minitest: Class Style and Spec Style.
# Class Style
class UserTest < MiniTest::Unit::TestCase
def some_method
user = User.create!(name: "Hi")
assert user.valid?
end
end
# Spec Style
describe User do
it 'can create a user'
user = User.create!(name: "Hi")
assert user.valid?
end
end
If your test database isn't set up already, you'll need to set it up by running rails db:test:prepare
.
Now we can run our test by typing rails test test/models/user_test.rb
.
Create a new rake file and insert the following code vim lib/tasks/minitest.rake
:
# lib/tasks/minitest.rake
require "rake/testtask"
# Will automatically prepare test database if there are new migrations
# Searches for files ending in _test.rb in the test directory
Rake::TestTask.new(:test => "db:test:prepare") do |t|
t.libs << "test"
t.pattern << = "test/**/*_test.rb"
end
# Will use test as defaut task if rake is run by itself
task :default => :test
Install capybara in your gemfile
.
group :test do
gem 'minitest'
gem 'capybara'
end
Insert a special class into the minitest_helper.rb
# Add this below minitest/autorun
require "capybara/rails"
class IntegrationTest < MiniTest::Spec
# Gibes us access to rails path helpers
include Rails.application.routes.url_helpers
# Gives us access to visit pages and expect methods
include Capybara::DSL
# This will use a regex expression to find tests that have
# (describe 'users integrations' do) and add in this extra
# functionality
register_spec_type /integration$/, self
end
Create a mkdir test/integration && vim users_integration_test.rb
.
require "minitest_helper"
describe "Users integration" do
it "shows the users name" do
user = User.create!(name: "Hi")
visit users_path(user)
page.text.must_include "Hi"
end
end
Add the following lines to test/minitest_helper.rb
# Add this below capybara/rails
require "active_support/testing/setup_and_teardown"
class HelperTest < MiniTest::Spec
# This will import all the rails helpers
include ActiveSupport::Tseting::SetupAndTeardown
include ActionView::TestCase::Behavior
register_spec_type(/Helper$/, self)
end
Create a new folder and file mkdir test/helpers && vim users_helper_test.rb
.
require "minitest_helper"
describe UsersHelper do
it "converts users income" do
number_to_currency(5000).must_equal "$5.00"
end
end
You can skip tests one of two ways:
it "converts users income"
it "converts users income" do
skip ""
number_to_currency(5000).must_equal "$5.00"
end
After running the test specs again rails test
, you'll see an 'S' on the specs you have skipped.
You'll notice that you will have --seed 12354 (this number will change each time the test is run), this is because minitest will randomize the order in which the tests are run to make sure tests are not failing or succeeding due to "state carry over" of a previously run test.
You can run the same instance of test sequences again by using rails TESTOPTS='--seed 12354'
Install turn in your gemfile
.
group :test do
gem 'minitest'
gem 'capybara'
gem 'turn'
end
Then run bundle
.
If you don't like the default layout, you can add several differnt options to customize the output. This can be done in your minitest_helper.rb
.
Turn.config.format = :outline
When you run the tests again you'll see that the test output is now different.
https://gist.github.com/821558
This gem can also be used to have Rspec like expecations.
page.should have_content('Title')
You'll want to add in the gems required to use minitest rails gem along with capybara for integration testing. Poltergeist is a driver for Capybara that allows you to run your tests on a headless WebKit browser, which is provided by PhantomJS (which may need to be installed additionally). Awesome Print will adjust how our test results display in the terminal. Database cleaner is added to rollback any changes we make to the test database during our runs.
# gemfile
group :development, :test do
gem 'minitest'
gem 'minitest-rails'
gem 'minitest-reporters'
gem 'poltergeist', '~> 1.8.1'
gem 'awesome_print'
gem "minitest-rails-capybara"
end
group :test do
gem 'database_cleaner'
end
Then create a test folder and add a new file called minitest_helper. This file will be included in each _test file we create from here on out. It will store all of our configurations to run our tests.
# test/minitest_helper.rb
ENV["RAILS_ENV"] = "test"
require File.expand_path("../../config/environment", __FILE__)
require "rails/test_help"
require "minitest/rails"
require "minitest/rails/capybara"
require "minitest/pride"
require "minitest/reporters"
require 'capybara/rails'
require 'capybara/poltergeist'
require 'database_cleaner'
class ActiveSupport::TestCase
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
# Add more helper methods to be used by all tests here...
Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
ActiveRecord::Migration.check_pending!
fixtures :all
end
class ActionDispatch::IntegrationTest
# Devise integration is no longer needed in the newest rails version
include Devise::Test::IntegrationHelpers
include Capybara::DSL
Capybara.register_driver :poltergeist do |app|
Capybara::Poltergeist::Driver.new(app, :js_errors => false, :phantomjs_options => ['--load-images=no', '--ignore-ssl-errors=yes'])
end
Capybara.default_max_wait_time = 5
Capybara.current_driver = :poltergeist
Capybara.app_host = "http://localhost:3003"
# To test action cable in rails, we need a multi threaded server
Capybara.server = :puma
Capybara.server_host = "localhost"
Capybara.server_port = "3003"
self.use_transactional_tests = false
def screenshot(page)
`open #{page.save_screenshot("log/#{Time.now.to_f}.png", :full => true)}`
end
DatabaseCleaner.strategy = :truncation
before :each do
DatabaseCleaner.start
end
after :each do
DatabaseCleaner.clean
end
end
If we are wanting to stub a method and mock it being called as to not use the expense of any apis, we can do as follows below. This is save you having to hit your api during your tests and using up valuable query limits. In the code below, we will need to mock every variable that is being called in our original method. We will then set it to true to trick the test into passing it using the mocked object.
describe 'PlacesController' do
it 'mocks getting the geolocation upon creation' do
mock = Minitest::Mock.new
mock.expect :geocode, true
mock.expect :lat, true
mock.expect :lng, true
mock.expect :state, true
mock.expect :city, true
mock.expect :zip, true
Place::MultiGeocoder.stub :geocode, mock do
expect {
post places_url, params: { place: { city: "Salt Lake City", state: "Utah" } }
}.must_change "Place.count"
end
end
end
Here's a good additional resource on mocking and stubbing
Another great guide is the default documentation found here on Ruby on Rails's Website.
A nice cheat sheet I found from a generous Github user.
=Navigating=
visit('/projects')
visit(post_comments_path(post))
=Clicking links and buttons=
click_link('id-of-link')
click_link('Link Text')
click_button('Save')
click('Link Text') # Click either a link or a button
click('Button Value')
=Interacting with forms=
fill_in('First Name', :with => 'John')
fill_in('Password', :with => 'Seekrit')
fill_in('Description', :with => 'Really Long Text…')
choose('A Radio Button')
check('A Checkbox')
uncheck('A Checkbox')
attach_file('Image', '/path/to/image.jpg')
select('Option', :from => 'Select Box')
=scoping=
within("//li[@id='employee']") do
fill_in 'Name', :with => 'Jimmy'
end
within(:css, "li#employee") do
fill_in 'Name', :with => 'Jimmy'
end
within_fieldset('Employee') do
fill_in 'Name', :with => 'Jimmy'
end
within_table('Employee') do
fill_in 'Name', :with => 'Jimmy'
end
=Querying=
page.has_xpath?('//table/tr')
page.has_css?('table tr.foo')
page.has_content?('foo')
page.should have_xpath('//table/tr')
page.should have_css('table tr.foo')
page.should have_content('foo')
page.should have_no_content('foo')
find_field('First Name').value
find_link('Hello').visible?
find_button('Send').click
find('//table/tr').click
locate("//*[@id='overlay'").find("//h1").click
all('a').each { |a| a[:href] }
=Scripting=
result = page.evaluate_script('4 + 4');
=Debugging=
save_and_open_page
=Asynchronous JavaScript=
click_link('foo')
click_link('bar')
page.should have_content('baz')
page.should_not have_xpath('//a')
page.should have_no_xpath('//a')
=XPath and CSS=
within(:css, 'ul li') { ... }
find(:css, 'ul li').text
locate(:css, 'input#name').value
Capybara.default_selector = :css
within('ul li') { ... }
find('ul li').text
locate('input#name').value
# Integration testing
it "verifies the navbar can be accessed on mobile devices" do
# Resize the screen in the test to mobile size
Capybara.page.driver.browser.resize(375, 667)
visit root_path
assert_nil(find(".navbar-toggle")["aria-expanded"])
find(".navbar-toggle").click
find(".navbar-toggle")["aria-expanded"].must_equal "true"
click_link("LINK")
# Search within a given div (helps avoid if multiple links have the same value on page)
# match: :first - returns the first occurrence of .dropdown, since navbar have more than one
# and in this case, we want to test the first one
within(".dropdown", match: :first) do
page.find('a[href="/link"]')['aria-expanded'].must_equal "true"
page.has_link?(href: '/link', visible: true).must_equal true
page.has_link?(href: '/link', visible: true).must_equal true
end
end
# Test for any text on the page
page.has_content?("TEXT").must_equal true
# Test if a link is not visible on a page
page.has_link?(href: '/link', visible: false).must_equal true
# Find an image with a link and use regex to test it's file name against the src
assert_match(/image_file_name/, page.find('a[href="/link"] img')['src'])
# Check if a button is disabled on the page
page.has_css?("#button-id[disabled]").must_equal true
# Check mark a field by #id
check('id_without_#')
page.has_selector?('#name_form').must_equal true
page.has_css?("h1", text: "TEXT").must_equal true
page.has_css?(".class", count: 4)
# To iterate over x amount of selectors, you'll want to use this
page.all(:css, "selector_path").each_with_index do |elem, index|
end
# Click on a div or an item
within ("#div-name") do
find('#element').click
end
# Controller Testing
before do
sign_in(user)
end
let(:post_variable) { post :one }
it "gets index" do
get post_index_url
value(response).must_be :success?
end
it "creates post" do
expect {
post post_index_url, params: { post_name: { title: "This Baby", text: "My better.", image: fixture_file_upload(Rails.root.join('app', 'assets', 'images', 'image.jpg'), 'image/jpg'), title: "hi", date: Time.now } }
}.must_change "Post.count"
must_redirect_to post_path
end
it "destroys post" do
expect {
delete post_index_url(post_variable)
}.must_change "Post.count", -1
must_redirect_to post_index_path
end
it "updates post" do
patch post_url(post_variable), params: { post_name: { title: "This Baby", text: "My better.", image: fixture_file_upload(Rails.root.join('app', 'assets', 'images', 'image.jpg'), 'image/jpg'), title: "hi", date: Time.now } }
must_redirect_to post_path(post_variable)
end