Install and Use MiniTest


Mini Test

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.

Installing Minitest

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"

Creating Model Tests

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.

Creating a Rake Task To Run Certain Tests

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

Integration testing with Capybara

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

Helper Testing

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

Skipping Tests

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.

--seed 13534

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 gem to improve test display

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.

Fixtures

https://gist.github.com/821558

Capybara MiniTest Spec

This gem can also be used to have Rspec like expecations.

page.should have_content('Title')

Alternative Setup For MiniTest Rails

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

Stubbing and Mocking

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


Documentation

Another great guide is the default documentation found here on Ruby on Rails's Website.

Capybara Cheatsheet

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

Additional Minitest Commands

# 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