Ruby rails: testing

From wikinotes

Rails provides testing facilities built overtop of ruby minitest.
Test cases are subclasses of ActiveSupport::TestCase (which is a subclass of Minitest::Test).

Documentation

official docs https://guides.rubyonrails.org/testing.html
rails-minitest assertions https://guides.rubyonrails.org/testing.html#available-assertions
rails assertions https://guides.rubyonrails.org/testing.html#rails-specific-assertions

Locations

Test Data
{project}/test/factories/{model}.rb factorybot pre-made models (not technically rails)
{project}/test/fixtures/{table}.yml pre-made models from provided data.
Test Types
{project}/test/{component}/{component}_test.rb unittests for models, controllers, helpers, ...
{project}/test/integration/ integration tests (ActionDispatch::IntegrationTest)
{project}/test/system/{model}_test.rb system tests (...ui tests with selenium?...)

Usage

Test Runners

NOTE:

If debugging flaky tests, config.active_support.test_order = :sorted forces tests to run in a non-random order

NOTE:

The rails test command is defined in the railties package.

# =========
# unittests
# =========
rails test tests/test_file.rb      # run testfile
rails test tests/test_file.rb:99   # run testfile, test at line 99

# some other options
rails test \
  -b  `# print backtrace for each failure` \
  -n /matches_regex/  \
  -v  `# show tests as they run` \
  tests/test_file.rb


# ============
# system tests
# ============
rails test:system                 # run all system tests

You can also manually run tests, *with extra cli-options* with the following code from railties

$LOAD_PATH << Rails.root.join("test")
args = ["test/some/awesome_test.rb", "--some-cli-arg", "--some-other-arg"]
Rails::TestUnit::Runner.parse_options(args)
Rails::TestUnit::Runner.run(args)

Generators

rails generate integration_test blog_flow  # gen integration test

Methods

TODO:

maybe this deserve it's own section?

test "does thing" do
  skip "skip this test because reasons.."
end

Sample

Models

require 'test_helper'
 
class ArticleTest < ActiveSupport::TestCase
  setup do             # def setup
    @foo = "bar"
    User.destroy_all   # destroy all instances of users before each test
  end

  test "the truth" do  # def test_the_ruth
    assert true
  end
end

Controllers

class ArticleControllerTest < ActionController::TestCase
end

Basics

Assertions

By default, rails testcases support all minitest assertions, and some general rails-specific tests. (See https://guides.rubyonrails.org/testing.html#rails-specific-assertions ). See ruby minitest .


You may also include more specific tests:

TestCase Supplemental Assertions
ActiveSupport::TestCase (base) ActiveSupport::Testing::Assertions
ActionMailer::TestCase ActionMailer::TestHelper

ActionDispatch::Assertions::SelectorAssertions

ActionView::TestCase none
ActionController::TestCase Rails::Dom::Testing

simple selector tutorial

ActiveJob::TestCase ActiveJob::TestHelper
ActionDispatch::IntegrationTest none?
ActionDispatch::SystemTestCase
Rails::Generators::TestCase

Some useful, universally available assertions.

# asserts that article.count increases by +2
# after the block completes
assert_difference ->{ Article.count }, 2 do
  post :create, params: { article: {...} }
end

Model Fixtures

Using a similar name/path convention as models/views/controllers, rails uses YAML files to define ready-made test-data.

sample model

# {project}/app/models/user.rb
class User < ActiveRecord::Base
  # columns:
  #   first_name: str
  #   last_name:  str
end

define fixtures for model

# {project}/test/fixtures/users.yml
#
# file is preprocessed with eruby
alex:
  first_name: Alex
  last_name: Guthrie

courtney:
  first_name: Courtney
  last_name: Breau

use fixtures within test

# {project}/test/controllers/user_controller.rb
class UserControllerTest < ?
  test "user alex" do
    user = users(:alex)                       # <-- refers to YAML file
    alex, courtney = users(:alex, :courtney)  # <-- refers to YAML file
    assert_equal user.first_name, "Alex"
    assert_equal user.last_name, "Guthrie"
  end
end

use fixtures in rails console

# build all fixtures from '/test/fixtures/users.yml'
rake db:fixtures:load FIXTURES=users

# build all fixtures from 'test/fixtures/admin/users.yml'
rake db:fixtures:load \
  FIXTURES_DIR=admin \
  FIXTURES=admin/users
rails console                     # open 'dev' rails console

env RAILS_ENV=test rails console  # open 'test' rails console

RAILS_ENV=development bin/rails db:fixtures:load  # load fixtures into dev db


Model Factories

Just a note, ruby factorybot is often used in place of fixtures.

Test Types

NOTE:

The best ways of learning available options to each test are:

  • Read API docs for TestCase (very good overview)
  • binding.pry within a test case, issue ls
    (cd ActiveController::TestCase only shows class-methods!)

Controller Tests

Helpers

post(:show, params: { a: 1, b: 2 }) # get, post, patch, put, ...

URL helpers

You won't be able to complete these in pry, but you can use URL helpers in tests.
Unfortunately, the naming convention is slightly different than in production.

assert_redirected_to ${namespace}_${controller}_path  # nested resources
assert_redirected_to action: :new                     # on same controller

Nested Resource Controllers

When testing a path to a nested-resource, you must include the ID of the resource above it in params.

class HouseControllerTest < ActionController::TestCase
  test "..." do
    # POST https://domain.com/family/1/house#create
    post(:create, params: { family: 1 }
  end
end

Debugging Rails Magic

# break when controller is about to call action during tests
# ==========================================================
break AbstractController::Callbacks#process_action  # actionpack-X.X.X.X/lib/abstract_controller/callbacks.rb
# or
break ${YOUR_CONTROLLER}#process_action

View Tests

Also applicable to emails.

Selector Assertions

Selectors target a type of HTML element, it's attributes between brackets.
For example (p, form, div, tr, ...).

# assert 1x form-input w/ value
assert_select(“form input[type=submit][value=‘Send Reset Instructions’]”, 1)  

# assert select form with values
assert_select("form", id: "new_cat", action: "/cats/#{cat.id}") do
  assert_select("input[type=date][name*=birthday][value=\"1970-01-01\"]", 1)
  assert_select("input[type=checkbox][name*=healthy][value=1]", 1)
end

Selector Syntax

assert_select("div#my-div",      1) # <div id=my-div>
assert_select("div.my-class",    1) # <div class=my-class>
assert_select("div[name]",       1) # <div name=${ANYTHING}>
assert_select("div[name^=foo]",  1) # div, where name starts with 'foo'
assert_select("div[name$=bar]",  1) # div, where name ends with 'bar'
assert_select("div[name*=oba]",  1) # div, where name contains 'oba'
assert_select("div[name~=baz]",  1) # div, where name contains 'baz' (space separated word)
assert_select("div[name=foobar], 1) # div, where name=foobar

Model Tests

Migration Tests

TODO:

learn. also, learn how to seed only test database.

Job Tests

perform param_a: 1, param_b: 2  # run job within test (is normally queued)

Email Tests

See Also: View Tests

email.body.parts  # 0=text/plain 1=text/html
email.html_part.decoded  # raw html

email_contents = ::Nokogiri::DocumentFragment.parse(email.html_part.decoded)
assert_select email_contents, ":root" do
  assert_select 'p', /part of email/
end

email.deliver_now
assert_select_email do
  assert_select 'p', /part of email/
end

Log Tests