Movatterモバイル変換


[0]ホーム

URL:


Skip to main content
More atrubyonrails.org:

Testing Rails Applications

This guide explores how to write tests in Rails.

After reading this guide, you will know:

  • Rails testing terminology.
  • How to write unit, functional, integration, and system tests for yourapplication.
  • Other popular testing approaches and plugins.

1. Why Write Tests?

Writing automated tests can be a faster way of ensuring your code continues towork as expected than manual testing through the browser or the console. Failingtests can quickly reveal issues, allowing you to identify and fix bugs early inthe development process. This practice not only improves the reliability of yourcode but also improves confidence in your changes.

Rails makes it easy to write tests. You can read more about Rails' built insupport for testing in the next section.

2. Introduction to Testing

With Rails, testing is central to the development process right from thecreation of a new application.

2.1. Test Setup

Rails creates atest directory for you as soon as you create a Rails projectusingbin/rails newapplication_name. If you list the contents of this directorythen you will see:

$ls-Ftestcontrollers/                     helpers/                         mailers/                         fixtures/                        integration/                     models/                          test_helper.rb

2.2. Test Directories

Thehelpers,mailers, andmodels directories store tests forviewhelpers,mailers, andmodels, respectively.

Thecontrollers directory is used fortests related to controllers, routes, andviews, where HTTP requests will be simulated and assertions made on theoutcomes.

Theintegration directory is reserved fortests that coverinteractions between controllers.

Thesystem test directory holdssystem tests, which areused for full browser testing of your application. System tests allow you totest your application the way your users experience it and help you test yourJavaScript as well. System tests inherit fromCapybara and perform in-browsertests for your application.

Fixturesare a way of mocking up data to use in your tests, so that you don't have to use'real' data. They are stored in thefixtures directory, and you can read moreabout them in theFixtures section below.

Ajobs directory will also be created for your job tests when you firstgenerate a job.

Thetest_helper.rb file holds the default configuration for your tests.

Theapplication_system_test_case.rb holds the default configuration for yoursystem tests.

2.3. The Test Environment

By default, every Rails application has three environments: development, test,and production.

Each environment's configuration can be modified similarly. In this case, we canmodify our test environment by changing the options found inconfig/environments/test.rb.

Your tests are run underRAILS_ENV=test. This is set by Rails automatically.

2.4. Writing Your First Test

We introduced thebin/rails generate model command in theGetting Startedwith Rails guide.Alongside creating a model, this command also creates a test stub in thetestdirectory:

$bin/railsgenerate model article title:string body:text...create  app/models/article.rbcreate  test/models/article_test.rb...

The default test stub intest/models/article_test.rb looks like this:

require"test_helper"classArticleTest<ActiveSupport::TestCase# test "the truth" do#   assert true# endend

A line by line examination of this file will help get you oriented to Railstesting code and terminology.

require"test_helper"

Requiring the file,test_helper.rb, loads the default configuration to runtests. All methods added to this file are also available in tests when this fileis included.

classArticleTest<ActiveSupport::TestCase# ...end

This is called a test case, because theArticleTest class inherits fromActiveSupport::TestCase. It therefore also has all the methods fromActiveSupport::TestCase available to it.Later in thisguide, we'll see some of the methods this gives us.

Any method defined within a class inherited fromMinitest::Test (which is thesuperclass ofActiveSupport::TestCase) that begins withtest_ is simplycalled a test. So, methods defined astest_password andtest_valid_passwordare test names and are run automatically when the test case is run.

Rails also adds atest method that takes a test name and a block. It generatesa standardMinitest::Unit test with method names prefixed withtest_,allowing you to focus on writing the test logic without having to think aboutnaming the methods. For example, you can write:

test"the truth"doasserttrueend

Which is approximately the same as writing this:

deftest_the_truthasserttrueend

Although you can still use regular method definitions, using thetest macroallows for a more readable test name.

The method name is generated by replacing spaces with underscores. Theresult does not need to be a valid Ruby identifier, as Ruby allows any string toserve as a method name, including those containing punctuation characters. Whilethis may require usingdefine_method andsend to define and invoke suchmethods, there are few formal restrictions on the names themselves.

This part of a test is called an 'assertion':

asserttrue

An assertion is a line of code that evaluates an object (or expression) forexpected results. For example, an assertion can check:

  • does this value equal that value?
  • is this object nil?
  • does this line of code throw an exception?
  • is the user's password greater than 5 characters?

Every test may contain one or more assertions, with no restriction as to howmany assertions are allowed. Only when all the assertions are successful willthe test pass.

2.4.1. Your First Failing Test

To see how a test failure is reported, you can add a failing test to thearticle_test.rb test case. In this example, it is asserted that the articlewill not save without meeting certain criteria; hence, if the article savessuccessfully, the test will fail, demonstrating a test failure.

require"test_helper"classArticleTest<ActiveSupport::TestCasetest"should not save article without title"doarticle=Article.newassert_notarticle.saveendend

Here is the output if this newly added test is run:

$bin/rails test test/models/article_test.rbRunning 1 tests in a single process (parallelization threshold is 50)Run options: --seed 44656# Running:FFailure:ArticleTest#test_should_not_save_article_without_title [/path/to/blog/test/models/article_test.rb:4]:Expected true to be nil or falsebin/rails test test/models/article_test.rb:4Finished in 0.023918s, 41.8090 runs/s, 41.8090 assertions/s.1 runs, 1 assertions, 1 failures, 0 errors, 0 skips

In the output,F indicates a test failure. The section underFailureincludes the name of the failing test, followed by a stack trace and a messageshowing the actual value and the expected value from the assertion. The defaultassertion messages offer just enough information to help identify the error. Forimproved readability, every assertion allows an optional message parameter tocustomize the failure message, as shown below:

test"should not save article without title"doarticle=Article.newassert_notarticle.save,"Saved the article without a title"end

Running this test shows the friendlier assertion message:

Failure:ArticleTest#test_should_not_save_article_without_title [/path/to/blog/test/models/article_test.rb:6]:Saved the article without a title

To get this test to pass a model-level validation can be added for thetitlefield.

classArticle<ApplicationRecordvalidates:title,presence:trueend

Now the test should pass, as the article in our test has not been initializedwith atitle, so the model validation will prevent the save. This can beverified by running the test again:

$bin/rails test test/models/article_test.rb:6Running 1 tests in a single process (parallelization threshold is 50)Run options: --seed 31252# Running:.Finished in 0.027476s, 36.3952 runs/s, 36.3952 assertions/s.1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

The small green dot displayed means that the test has passed successfully.

In the process above, a test was written first which fails for a desiredfunctionality, then after, some code was written which adds the functionality.Finally, the test was run again to ensure it passes. This approach to softwaredevelopment is referred to asTest-Driven Development (TDD).

2.4.2. Reporting Errors

To see how an error gets reported, here's a test containing an error:

test"should report error"do# some_undefined_variable is not defined elsewhere in the test casesome_undefined_variableasserttrueend

Now you can see even more output in the console from running the tests:

$bin/rails test test/models/article_test.rbRunning 2 tests in a single process (parallelization threshold is 50)Run options: --seed 1808# Running:EError:ArticleTest#test_should_report_error:NameError: undefined local variable or method 'some_undefined_variable' for #<ArticleTest:0x007fee3aa71798>    test/models/article_test.rb:11:in 'block in <class:ArticleTest>'bin/rails test test/models/article_test.rb:9.Finished in 0.040609s, 49.2500 runs/s, 24.6250 assertions/s.2 runs, 1 assertions, 0 failures, 1 errors, 0 skips

Notice the 'E' in the output. It denotes a test with an error. The green dotabove the 'Finished' line denotes the one passing test.

The execution of each test method stops as soon as any error or anassertion failure is encountered, and the test suite continues with the nextmethod. All test methods are executed in random order. Theconfig.active_support.test_order option can be used to configure testorder.

When a test fails you are presented with the corresponding backtrace. Bydefault, Rails filters the backtrace and will only print lines relevant to yourapplication. This eliminates noise and helps you to focus on your code. However,in situations when you want to see the full backtrace, set the-b (or--backtrace) argument to enable this behavior:

$bin/rails test-btest/models/article_test.rb

If you want this test to pass you can modify it to useassert_raises (so youare now checking for the presence of the error) like so:

test"should report error"do# some_undefined_variable is not defined elsewhere in the test caseassert_raises(NameError)dosome_undefined_variableendend

This test should now pass.

2.5. Minitest Assertions

By now you've caught a glimpse of some of the assertions that are available.Assertions are the foundation blocks of testing. They are the ones that actuallyperform the checks to ensure that things are going as planned.

Here's an extract of the assertions you can use withminitest, the default testing libraryused by Rails. The[msg] parameter is an optional string message you canspecify to make your test failure messages clearer.

AssertionPurpose
assert(test, [msg])Ensures thattest is true.
assert_not(test, [msg])Ensures thattest is false.
assert_equal(expected, actual, [msg])Ensures thatexpected == actual is true.
assert_not_equal(expected, actual, [msg])Ensures thatexpected != actual is true.
assert_same(expected, actual, [msg])Ensures thatexpected.equal?(actual) is true.
assert_not_same(expected, actual, [msg])Ensures thatexpected.equal?(actual) is false.
assert_nil(obj, [msg])Ensures thatobj.nil? is true.
assert_not_nil(obj, [msg])Ensures thatobj.nil? is false.
assert_empty(obj, [msg])Ensures thatobj isempty?.
assert_not_empty(obj, [msg])Ensures thatobj is notempty?.
assert_match(regexp, string, [msg])Ensures that a string matches the regular expression.
assert_no_match(regexp, string, [msg])Ensures that a string doesn't match the regular expression.
assert_includes(collection, obj, [msg])Ensures thatobj is incollection.
assert_not_includes(collection, obj, [msg])Ensures thatobj is not incollection.
assert_in_delta(expected, actual, [delta], [msg])Ensures that the numbersexpected andactual are withindelta of each other.
assert_not_in_delta(expected, actual, [delta], [msg])Ensures that the numbersexpected andactual are not withindelta of each other.
assert_in_epsilon(expected, actual, [epsilon], [msg])Ensures that the numbersexpected andactual have a relative error less thanepsilon.
assert_not_in_epsilon(expected, actual, [epsilon], [msg])Ensures that the numbersexpected andactual have a relative error not less thanepsilon.
assert_throws(symbol, [msg]) { block }Ensures that the given block throws the symbol.
assert_raises(exception1, exception2, ...) { block }Ensures that the given block raises one of the given exceptions.
assert_instance_of(class, obj, [msg])Ensures thatobj is an instance ofclass.
assert_not_instance_of(class, obj, [msg])Ensures thatobj is not an instance ofclass.
assert_kind_of(class, obj, [msg])Ensures thatobj is an instance ofclass or is descending from it.
assert_not_kind_of(class, obj, [msg])Ensures thatobj is not an instance ofclass and is not descending from it.
assert_respond_to(obj, symbol, [msg])Ensures thatobj responds tosymbol.
assert_not_respond_to(obj, symbol, [msg])Ensures thatobj does not respond tosymbol.
assert_operator(obj1, operator, [obj2], [msg])Ensures thatobj1.operator(obj2) is true.
assert_not_operator(obj1, operator, [obj2], [msg])Ensures thatobj1.operator(obj2) is false.
assert_predicate(obj, predicate, [msg])Ensures thatobj.predicate is true, e.g.assert_predicate str, :empty?
assert_not_predicate(obj, predicate, [msg])Ensures thatobj.predicate is false, e.g.assert_not_predicate str, :empty?
flunk([msg])Ensures failure. This is useful to explicitly mark a test that isn't finished yet.

The above are a subset of assertions that minitest supports. For an exhaustiveand more up-to-date list, please check theminitest APIdocumentation, specificallyMinitest::Assertions.

With minitest you can add your own assertions. In fact, that's exactly whatRails does. It includes some specialized assertions to make your life easier.

Creating your own assertions is a topic that we won't cover in depth inthis guide.

2.6. Rails-Specific Assertions

Rails adds some custom assertions of its own to theminitest framework:

AssertionPurpose
assert_difference(expressions, difference = 1, message = nil) {...}Test numeric difference between the return value of an expression as a result of what is evaluated in the yielded block.
assert_no_difference(expressions, message = nil, &block)Asserts that the numeric result of evaluating an expression is not changed before and after invoking the passed in block.
assert_changes(expressions, message = nil, from:, to:, &block)Test that the result of evaluating an expression is changed after invoking the passed in block.
assert_no_changes(expressions, message = nil, &block)Test the result of evaluating an expression is not changed after invoking the passed in block.
assert_nothing_raised { block }Ensures that the given block doesn't raise any exceptions.
assert_recognizes(expected_options, path, extras = {}, message = nil)Asserts that the routing of the given path was handled correctly and that the parsed options (given in the expected_options hash) match path. Basically, it asserts that Rails recognizes the route given by expected_options.
assert_generates(expected_path, options, defaults = {}, extras = {}, message = nil)Asserts that the provided options can be used to generate the provided path. This is the inverse of assert_recognizes. The extra parameter is used to tell the request the names and values of additional request parameters that would be in a query string. The message parameter allows you to specify a custom error message for assertion failures.
assert_routing(expected_path, options, defaults = {}, extras = {}, message = nil)Asserts thatpath andoptions match both ways; in other words, it verifies thatpath generatesoptions and then thatoptions generatespath. This essentially combinesassert_recognizes andassert_generates into one step. The extras hash allows you to specify options that would normally be provided as a query string to the action. The message parameter allows you to specify a custom error message to display upon failure.
assert_response(type, message = nil)Asserts that the response comes with a specific status code. You can specify:success to indicate 200-299,:redirect to indicate 300-399,:missing to indicate 404, or:error to match the 500-599 range. You can also pass an explicit status number or its symbolic equivalent. For more information, seefull list of status codes and how theirmapping works.
assert_redirected_to(options = {}, message = nil)Asserts that the response is a redirect to a URL matching the given options. You can also pass named routes such asassert_redirected_to root_path and Active Record objects such asassert_redirected_to @article.
assert_queries_count(count = nil, include_schema: false, &block)Asserts that&block generates anint number of SQL queries.
assert_no_queries(include_schema: false, &block)Asserts that&block generates no SQL queries.
assert_queries_match(pattern, count: nil, include_schema: false, &block)Asserts that&block generates SQL queries that match the pattern.
assert_no_queries_match(pattern, &block)Asserts that&block generates no SQL queries that match the pattern.
assert_error_reported(class) { block }Asserts that the error class has been reported, e.g.assert_error_reported IOError { Rails.error.report(IOError.new("Oops")) }
assert_no_error_reported { block }Asserts that no errors have been reported, e.g.assert_no_error_reported { perform_service }

You'll see the usage of some of these assertions in the next chapter.

2.7. Assertions in Test Cases

All the basic assertions such asassert_equal defined inMinitest::Assertions are also available in the classes we use in our own testcases. In fact, Rails provides the following classes for you to inherit from:

Each of these classes includeMinitest::Assertions, allowing us to use all ofthe basic assertions in your tests.

For more information onminitest, refer to theminitestdocumentation.

2.8. The Rails Test Runner

We can run all of our tests at once by using thebin/rails test command.

Or we can run a single test file by appending the filename to thebin/railstest command.

$bin/rails test test/models/article_test.rbRunning 1 tests in a single process (parallelization threshold is 50)Run options: --seed 1559# Running:..Finished in 0.027034s, 73.9810 runs/s, 110.9715 assertions/s.2 runs, 3 assertions, 0 failures, 0 errors, 0 skips

This will run all test methods from the test case.

You can also run a particular test method from the test case by providing the-n or--name flag and the test's method name.

$bin/rails test test/models/article_test.rb-n test_the_truthRunning 1 tests in a single process (parallelization threshold is 50)Run options: -n test_the_truth --seed 43583# Running:.Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s.1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

You can also run a test at a specific line by providing the line number.

$bin/rails test test/models/article_test.rb:6# run specific test and line

You can also run a range of tests by providing the line range.

$bin/rails test test/models/article_test.rb:6-20# runs tests from line 6 to 20

You can also run an entire directory of tests by providing the path to thedirectory.

$bin/rails test test/controllers# run all tests from specific directory

The test runner also provides a lot of other features like failing fast, showingverbose progress, and so on. Check the documentation of the test runner usingthe command below:

$bin/rails test-hUsage:  bin/rails test [PATHS...]Run tests except system testsExamples:    You can run a single test by appending a line number to a filename:        bin/rails test test/models/user_test.rb:27    You can run multiple tests with in a line range by appending the line range to a filename:        bin/rails test test/models/user_test.rb:10-20    You can run multiple files and directories at the same time:        bin/rails test test/controllers test/integration/login_test.rb    By default test failures and errors are reported inline during a run.minitest options:    -h, --help                       Display this help.        --no-plugins                 Bypass minitest plugin auto-loading (or set $MT_NO_PLUGINS).    -s, --seed SEED                  Sets random seed. Also via env. Eg: SEED=n rake    -v, --verbose                    Verbose. Show progress processing files.        --show-skips                 Show skipped at the end of run.    -n, --name PATTERN               Filter run on /regexp/ or string.        --exclude PATTERN            Exclude /regexp/ or string from run.    -S, --skip CODES                 Skip reporting of certain types of results (eg E).Known extensions: rails, pride    -w, --warnings                   Run with Ruby warnings enabled    -e, --environment ENV            Run tests in the ENV environment    -b, --backtrace                  Show the complete backtrace    -d, --defer-output               Output test failures and errors after the test run    -f, --fail-fast                  Abort test run on first failure or error    -c, --[no-]color                 Enable color in the output        --profile [COUNT]            Enable profiling of tests and list the slowest test cases (default: 10)    -p, --pride                      Pride. Show your testing pride!

3. The Test Database

Just about every Rails application interacts heavily with a database and so yourtests will need a database to interact with as well. This section covers how toset up this test database and populate it with sample data.

As mentioned in theTest Environment section, everyRails application has three environments: development, test, and production. Thedatabase for each one of them is configured inconfig/database.yml.

A dedicated test database allows you to set up and interact with test data inisolation. This way your tests can interact with test data with confidence,without worrying about the data in the development or production databases.

3.1. Maintaining the Test Database Schema

In order to run your tests, your test database needs the current schema. Thetest helper checks whether your test database has any pending migrations. Itwill try to load yourdb/schema.rb ordb/structure.sql into the testdatabase. If migrations are still pending, an error will be raised. Usually thisindicates that your schema is not fully migrated. Running the migrations (usingbin/rails db:migrate RAILS_ENV=test) will bring the schema up to date.

If there were modifications to existing migrations, the test databaseneeds to be rebuilt. This can be done by executingbin/rails test:db.

3.2. Fixtures

For good tests, you'll need to give some thought to setting up test data. InRails, you can handle this by defining and customizing fixtures. You can findcomprehensive documentation in theFixtures APIdocumentation.

3.2.1. What are Fixtures?

Fixtures is a fancy word for a consistent set of test data. Fixtures allow you to populate yourtesting database with predefined data before your tests run. Fixtures aredatabase independent and written in YAML. There is one file per model.

Fixtures are not designed to create every object that your tests need, andare best managed when only used for default data that can be applied to thecommon case.

Fixtures are stored in yourtest/fixtures directory.

3.2.2. YAML

YAML is a human-readable data serialization language.YAML-formatted fixtures are a human-friendly way to describe your sample data.These types of fixtures have the.yml file extension (as inusers.yml).

Here's a sample YAML fixture file:

# lo & behold! I am a YAML comment!david:name:David Heinemeier Hanssonbirthday:1979-10-15profession:Systems developmentsteve:name:Steve Ross Kellockbirthday:1974-09-27profession:guy with keyboard

Each fixture is given a name followed by an indented list of colon-separatedkey/value pairs. Records are typically separated by a blank line. You can placecomments in a fixture file by using the # character in the first column.

If you are working withassociations, you can definea reference node between two different fixtures. Here's an example with abelongs_to/has_many association:

# test/fixtures/categories.ymlweb_frameworks:name:Web Frameworks
# test/fixtures/articles.ymlfirst:title:Welcome to Rails!category:web_frameworks
# test/fixtures/action_text/rich_texts.ymlfirst_content:record:first (Article)name:contentbody:<div>Hello, from <strong>a fixture</strong></div>

Notice thecategory key of thefirst Article found infixtures/articles.yml has a value ofweb_frameworks, and that therecord key of thefirst_content entry found infixtures/action_text/rich_texts.yml has a valueoffirst (Article). This hints to Active Record to load the Categoryweb_frameworksfound infixtures/categories.yml for the former, and Action Text to load theArticlefirst found infixtures/articles.yml for the latter.

For associations to reference one another by name, you can use the fixturename instead of specifying theid: attribute on the associated fixtures. Railswill auto-assign a primary key to be consistent between runs. For moreinformation on this association behavior please read theFixtures APIdocumentation.

3.2.3. File Attachment Fixtures

Like other Active Record-backed models, Active Storage attachment recordsinherit from ActiveRecord::Base instances and can therefore be populated byfixtures.

Consider anArticle model that has an associated image as athumbnailattachment, along with fixture data YAML:

classArticle<ApplicationRecordhas_one_attached:thumbnailend
# test/fixtures/articles.ymlfirst:title:An Article

Assuming that there is animage/png encoded file attest/fixtures/files/first.png, the following YAML fixture entries willgenerate the relatedActiveStorage::Blob andActiveStorage::Attachmentrecords:

# test/fixtures/active_storage/blobs.ymlfirst_thumbnail_blob: <%= ActiveStorage::FixtureSet.blob filename:"first.png"%>
# test/fixtures/active_storage/attachments.ymlfirst_thumbnail_attachment:name:thumbnailrecord:first (Article)blob:first_thumbnail_blob

3.2.4. Embedding Code in Fixtures

ERB allows you to embed Ruby code within templates. The YAML fixture format ispre-processed with ERB when Rails loads fixtures. This allows you to use Ruby tohelp you generate some sample data. For example, the following code generates athousand users:

<%1000.timesdo|n|%>  user_<%=n%>:    username:<%="user#{n}"%>    email:<%="user#{n}@example.com"%><%end%>

3.2.5. Fixtures in Action

Rails automatically loads all fixtures from thetest/fixtures directory bydefault. Loading involves three steps:

  1. Remove any existing data from the table corresponding to the fixture
  2. Load the fixture data into the table
  3. Dump the fixture data into a method in case you want to access it directly

In order to remove existing data from the database, Rails tries to disablereferential integrity triggers (like foreign keys and check constraints). If youare getting permission errors on running tests, make sure the database user hasthe permission to disable these triggers in the testing environment. (InPostgreSQL, only superusers can disable all triggers. Read more aboutpermissions in the PostgreSQLdocs).

3.2.6. Fixtures are Active Record Objects

Fixtures are instances of Active Record. As mentioned above, you can access theobject directly because it is automatically available as a method whose scope islocal to the test case. For example:

# this will return the User object for the fixture named davidusers(:david)# this will return the property for david called idusers(:david).id# methods available to the User object can also be accesseddavid=users(:david)david.call(david.partner)

To get multiple fixtures at once, you can pass in a list of fixture names. Forexample:

# this will return an array containing the fixtures david and steveusers(:david,:steve)

3.3. Transactions

By default, Rails automatically wraps tests in a database transaction that isrolled back once completed. This makes tests independent of each other and meansthat changes to the database are only visible within a single test.

classMyTest<ActiveSupport::TestCasetest"newly created users are active by default"do# Since the test is implicitly wrapped in a database transaction, the user# created here won't be seen by other tests.assertUser.create.active?endend

The methodActiveRecord::Base.current_transactionstill acts as intended, though:

classMyTest<ActiveSupport::TestCasetest"Active Record current_transaction method works as expected"do# The implicit transaction around tests does not interfere with the# application-level semantics of the current_transaction.assertUser.current_transaction.blank?endend

If there aremultiple writing databasesin place, tests are wrapped in as many respective transactions, and all of themare rolled back.

3.3.1. Opting-out of Test Transactions

Individual test cases can opt-out:

classMyTest<ActiveSupport::TestCase# No implicit database transaction wraps the tests in this test case.self.use_transactional_tests=falseend

4. Testing Models

Model tests are used to test the models of your application and their associatedlogic. You can test this logic using the assertions and fixtures that we'veexplored in the sections above.

Rails model tests are stored under thetest/models directory. Rails provides agenerator to create a model test skeleton for you.

$bin/railsgenerate test_unit:model articlecreate  test/models/article_test.rb

This command will generate the following file:

# article_test.rbrequire"test_helper"classArticleTest<ActiveSupport::TestCase# test "the truth" do#   assert true# endend

Model tests don't have their own superclass likeActionMailer::TestCase.Instead, they inherit fromActiveSupport::TestCase.

5. Functional Testing for Controllers

When writing functional tests, you are focusing on testing how controlleractions handle the requests and the expected result or response. Functionalcontroller tests are used to test controllers and other behavior, like API responses.

5.1. What to Include in Your Functional Tests

You could test for things such as:

  • was the web request successful?
  • was the user redirected to the right page?
  • was the user successfully authenticated?
  • was the correct information displayed in the response?

The easiest way to see functional tests in action is to generate a controllerusing the scaffold generator:

$bin/railsgenerate scaffold_controller article...create  app/controllers/articles_controller.rb...invoke  test_unitcreate    test/controllers/articles_controller_test.rb...

This will generate the controller code and tests for anArticle resource. Youcan take a look at the filearticles_controller_test.rb in thetest/controllers directory.

If you already have a controller and just want to generate the test scaffoldcode for each of the seven default actions, you can use the following command:

$bin/railsgenerate test_unit:scaffold article...invoke  test_unitcreate    test/controllers/articles_controller_test.rb...

if you are generating test scaffold code, you will see an@article valueis set and used throughout the test file. This instance ofarticle uses theattributes nested within a:one key in thetest/fixtures/articles.yml file.Make sure you have set the key and related values in this file before you try torun the tests.

Let's take a look at one such test,test_should_get_index from the filearticles_controller_test.rb.

# articles_controller_test.rbclassArticlesControllerTest<ActionDispatch::IntegrationTesttest"should get index"dogetarticles_urlassert_response:successendend

In thetest_should_get_index test, Rails simulates a request on the actioncalledindex, making sure the request was successful, and also ensuring thatthe right response body has been generated.

Theget method kicks off the web request and populates the results into the@response. It can accept up to 6 arguments:

  • The URI of the controller action you are requesting. This can be in the formof a string or a route helper (e.g.articles_url).
  • params: option with a hash of request parameters to pass into the action(e.g. query string parameters or article variables).
  • headers: for setting the headers that will be passed with the request.
  • env: for customizing the request environment as needed.
  • xhr: whether the request is AJAX request or not. Can be set to true formarking the request as AJAX.
  • as: for encoding the request with different content type.

All of these keyword arguments are optional.

Example: Calling the:show action (via aget request) for the firstArticle, passing in anHTTP_REFERER header:

getarticle_url(Article.first),headers:{"HTTP_REFERER"=>"http://example.com/home"}

Another example: Calling the:update action (via apatch request) for thelastArticle, passing in new text for thetitle inparams, as an AJAXrequest:

patcharticle_url(Article.last),params:{article:{title:"updated"}},xhr:true

One more example: Calling the:create action (via apost request) to createa new article, passing in text for thetitle inparams, as JSON request:

postarticles_url,params:{article:{title:"Ahoy!"}},as: :json

If you try running thetest_should_create_article test fromarticles_controller_test.rb it will (correctly) fail due to the newly addedmodel-level validation.

Now to modify thetest_should_create_article test inarticles_controller_test.rb so that this test passes:

test"should create article"doassert_difference("Article.count")dopostarticles_url,params:{article:{body:"Rails is awesome!",title:"Hello Rails"}}endassert_redirected_toarticle_path(Article.last)end

You can now run this test and it will pass.

If you followed the steps in theBasicAuthentication section, you'll needto add authorization to every request header to get all the tests passing:

postarticles_url,params:{article:{body:"Rails is awesome!",title:"Hello Rails"}},headers:{Authorization:ActionController::HttpAuthentication::Basic.encode_credentials("dhh","secret")}

5.2. HTTP Request Types for Functional Tests

If you're familiar with the HTTP protocol, you'll know thatget is a type ofrequest. There are 6 request types supported in Rails functional tests:

  • get
  • post
  • patch
  • put
  • head
  • delete

All of the request types have equivalent methods that you can use. In a typicalCRUD application you'll be usingpost,get,put, anddelete mostoften.

Functional tests do not verify whether the specified request type isaccepted by the action; instead, they focus on the result. For testing therequest type, request tests are available, making your tests more purposeful.

5.3. Testing XHR (AJAX) Requests

An AJAX request (Asynchronous JavaScript and XML) is a technique where content isfetched from the server using asynchronous HTTP requests and the relevant partsof the page are updated without requiring a full page load.

To test AJAX requests, you can specify thexhr: true option toget,post,patch,put, anddelete methods. For example:

test"AJAX request"doarticle=articles(:one)getarticle_url(article),xhr:trueassert_equal"hello world",@response.bodyassert_equal"text/javascript",@response.media_typeend

5.4. Testing Other Request Objects

After any request has been made and processed, you will have 3 Hash objectsready for use:

  • cookies - Any cookies that are set
  • flash - Any objects living in the flash
  • session - Any object living in session variables

As is the case with normal Hash objects, you can access the values byreferencing the keys by string. You can also reference them by symbol name. Forexample:

flash["gordon"]# or flash[:gordon]session["shmession"]# or session[:shmession]cookies["are_good_for_u"]# or cookies[:are_good_for_u]

5.5. Instance Variables

You also have access to three instance variables in your functional tests aftera request is made:

  • @controller - The controller processing the request
  • @request - The request object
  • @response - The response object
classArticlesControllerTest<ActionDispatch::IntegrationTesttest"should get index"dogetarticles_urlassert_equal"index",@controller.action_nameassert_equal"application/x-www-form-urlencoded",@request.media_typeassert_match"Articles",@response.bodyendend

5.6. Setting Headers and CGI Variables

HTTP headers are pieces of information sent along with HTTP requests to provideimportant metadata. CGI variables are environment variables used to exchangeinformation between the web server and the application.

HTTP headers and CGI variables can be tested by being passed as headers:

# setting an HTTP Headergetarticles_url,headers:{"Content-Type":"text/plain"}# simulate the request with custom header# setting a CGI variablegetarticles_url,headers:{"HTTP_REFERER":"http://example.com/home"}# simulate the request with custom env variable

5.7. Testingflash Notices

As can be seen in thetesting other request objectssection, one of the three hash objects that isaccessible in the tests isflash. This section outlines how to test theappearance of aflash message in our blog application whenever someonesuccessfully creates a new article.

First, an assertion should be added to thetest_should_create_article test:

test"should create article"doassert_difference("Article.count")dopostarticles_url,params:{article:{title:"Some title"}}endassert_redirected_toarticle_path(Article.last)assert_equal"Article was successfully created.",flash[:notice]end

If the test is run now, it should fail:

$bin/rails test test/controllers/articles_controller_test.rb-n test_should_create_articleRunning 1 tests in a single process (parallelization threshold is 50)Run options: -n test_should_create_article --seed 32266# Running:FFinished in 0.114870s, 8.7055 runs/s, 34.8220 assertions/s.  1) Failure:ArticlesControllerTest#test_should_create_article [/test/controllers/articles_controller_test.rb:16]:--- expected+++ actual@@ -1 +1 @@-"Article was successfully created."+nil1 runs, 4 assertions, 1 failures, 0 errors, 0 skips

Now implement the flash message in the controller. The:create action shouldlook like this:

defcreate@article=Article.new(article_params)if@article.saveflash[:notice]="Article was successfully created."redirect_to@articleelserender"new"endend

Now, if the tests are run they should pass:

$bin/rails test test/controllers/articles_controller_test.rb-n test_should_create_articleRunning 1 tests in a single process (parallelization threshold is 50)Run options: -n test_should_create_article --seed 18981# Running:.Finished in 0.081972s, 12.1993 runs/s, 48.7972 assertions/s.1 runs, 4 assertions, 0 failures, 0 errors, 0 skips

If you generated your controller using the scaffold generator, the flashmessage will already be implemented in yourcreate action.

5.8. Tests forshow,update, anddelete Actions

So far in the guide tests for the:index as well as the:create action havebeen outlined. What about the other actions?

You can write a test for:show as follows:

test"should show article"doarticle=articles(:one)getarticle_url(article)assert_response:successend

If you remember from our discussion earlier onfixtures, thearticles() method will provide access to the articles fixtures.

How about deleting an existing article?

test"should delete article"doarticle=articles(:one)assert_difference("Article.count",-1)dodeletearticle_url(article)endassert_redirected_toarticles_pathend

Here is a test for updating an existing article:

test"should update article"doarticle=articles(:one)patcharticle_url(article),params:{article:{title:"updated"}}assert_redirected_toarticle_path(article)# Reload article to refresh data and assert that title is updated.article.reloadassert_equal"updated",article.titleend

Notice that there is some duplication in these three tests - they both accessthe same article fixture data. It is possible to DRY ('Don't RepeatYourself') the implementation by using thesetup andteardown methodsprovided byActiveSupport::Callbacks.

The tests might look like this:

require"test_helper"classArticlesControllerTest<ActionDispatch::IntegrationTest# called before every single testsetupdo@article=articles(:one)end# called after every single testteardowndo# when controller is using cache it may be a good idea to reset it afterwardsRails.cache.clearendtest"should show article"do# Reuse the @article instance variable from setupgetarticle_url(@article)assert_response:successendtest"should destroy article"doassert_difference("Article.count",-1)dodeletearticle_url(@article)endassert_redirected_toarticles_pathendtest"should update article"dopatcharticle_url(@article),params:{article:{title:"updated"}}assert_redirected_toarticle_path(@article)# Reload association to fetch updated data and assert that title is updated.@article.reloadassert_equal"updated",@article.titleendend

Similar to other callbacks in Rails, thesetup andteardown methodscan also accept a block, lambda, or a method name as a symbol to be called.

6. Integration Testing

Integration tests take functional controller tests one step further - they focuson testing how several parts of an application interact, and are generally usedto test important workflows. Rails integration tests are stored in thetest/integration directory.

Rails provides a generator to create an integration test skeleton as follows:

$bin/railsgenerate integration_test user_flows      invoke  test_unit      create  test/integration/user_flows_test.rb

Here's what a freshly generated integration test looks like:

require"test_helper"classUserFlowsTest<ActionDispatch::IntegrationTest# test "the truth" do#   assert true# endend

Here the test is inheriting fromActionDispatch::IntegrationTest.This makes some additionalhelpers available for integrationtests alongside thestandard testing helpers.

6.1. Implementing an Integration Test

Let's add an integration test to our blog application, by starting with a basicworkflow of creating a new blog article to verify that everything is workingproperly.

Start by generating the integration test skeleton:

$bin/railsgenerate integration_test blog_flow

It should have created a test file placeholder. With the output of the previouscommand you should see:

      invoke  test_unit      create    test/integration/blog_flow_test.rb

Now open that file and write the first assertion:

require"test_helper"classBlogFlowTest<ActionDispatch::IntegrationTesttest"can see the welcome page"doget"/"assert_dom"h1","Welcome#index"endend

If you visit the root path, you should seewelcome/index.html.erb rendered forthe view. So this assertion should pass.

The assertionassert_dom (aliased toassert_select) is available in integration tests to checkthe presence of key HTML elements and their content.

6.1.1. Creating Articles Integration

To test the ability to create a new article in our blog and display theresulting article, see the example below:

test"can create an article"doget"/articles/new"assert_response:successpost"/articles",params:{article:{title:"can create",body:"article successfully."}}assert_response:redirectfollow_redirect!assert_response:successassert_dom"p","Title:\n  can create"end

The:new action of our Articles controller is called first, and the responseshould be successful.

Next, apost request is made to the:create action of the Articlescontroller:

post"/articles",params:{article:{title:"can create",body:"article successfully."}}assert_response:redirectfollow_redirect!

The two lines following the request are to handle the redirect setup whencreating a new article.

Don't forget to callfollow_redirect! if you plan to make subsequentrequests after a redirect is made.

Finally it can be asserted that the response was successful and thenewly-created article is readable on the page.

A very small workflow for visiting our blog and creating a new article wassuccessfully tested above. To extend this, additional tests could be added forfeatures like adding comments, editing comments or removing articles.Integration tests are a great place to experiment with all kinds of use casesfor our applications.

6.2. Helpers Available for Integration Tests

There are numerous helpers to choose from for use in integration tests. Someinclude:

7. System Testing

Similarly to integration testing, system testing allows you to test how thecomponents of your app work together, but from the point of view of a user. Itdoes this by running tests in either a real or a headless browser (a browserwhich runs in the background without opening a visible window). System tests useCapybara under the hood.

7.1. When to Use System Tests

System tests provide the most realistic testing experience as they test yourapplication from a user's perspective. However, they come with importanttrade-offs:

  • They are significantly slower than unit and integration tests
  • They can be brittle and prone to failures from timing issues or UI changes
  • They require more maintenance as your UI evolves

Given these trade-offs,system tests should be reserved for critical userpaths rather than being created for every feature. Consider writing systemtests for:

  • Core business workflows (e.g., user registration, checkout process,payment flows)
  • Critical user interactions that integrate multiple components
  • Complex JavaScript interactions that can't be tested at lower levels

For most features, integration tests provide a better balance of coverage andmaintainability. Save system tests for scenarios where you need to verify thecomplete user experience.

7.2. Generating System Tests

Rails no longer generates system tests by default when using scaffolds. Thischange reflects the best practice of using system tests sparingly. You cangenerate system tests in two ways:

  1. When scaffolding, explicitly enable system tests:
   $bin/railsgenerate scaffold Article title:string body:text--system-tests=true
  1. Generate system tests independently for critical features:
   $bin/railsgenerate system_test articles

Rails system tests are stored in thetest/system directory in yourapplication. To generate a system test skeleton, run the following command:

$bin/railsgenerate system_testusers      invoke test_unit      create test/system/users_test.rb

Here's what a freshly generated system test looks like:

require"application_system_test_case"classUsersTest<ApplicationSystemTestCase# test "visiting the index" do#   visit users_url##   assert_dom "h1", text: "Users"# endend

By default, system tests are run with the Selenium driver, using the Chromebrowser, and a screen size of 1400x1400. The next section explains how to changethe default settings.

7.3. Changing the Default Settings

Rails makes changing the default settings for system tests very simple. All thesetup is abstracted away so you can focus on writing your tests.

When you generate a new application or scaffold, anapplication_system_test_case.rb file is created in the test directory. This iswhere all the configuration for your system tests should live.

If you want to change the default settings, you can change what the system testsare "driven by". If you want to change the driver from Selenium to Cuprite,you'd add thecuprite gem to yourGemfile. Then in yourapplication_system_test_case.rb file you'd do thefollowing:

require"test_helper"require"capybara/cuprite"classApplicationSystemTestCase<ActionDispatch::SystemTestCasedriven_by:cupriteend

The driver name is a required argument fordriven_by. The optional argumentsthat can be passed todriven_by are:using for the browser (this will onlybe used by Selenium),:screen_size to change the size of the screen forscreenshots, and:options which can be used to set options supported by thedriver.

require"test_helper"classApplicationSystemTestCase<ActionDispatch::SystemTestCasedriven_by:selenium,using: :firefoxend

If you want to use a headless browser, you could use Headless Chrome or HeadlessFirefox by addingheadless_chrome orheadless_firefox in the:usingargument.

require"test_helper"classApplicationSystemTestCase<ActionDispatch::SystemTestCasedriven_by:selenium,using: :headless_chromeend

If you want to use a remote browser, e.g.Headless Chrome inDocker, you have to add a remoteurl and setbrowser as remote throughoptions.

require"test_helper"classApplicationSystemTestCase<ActionDispatch::SystemTestCaseurl=ENV.fetch("SELENIUM_REMOTE_URL",nil)options=ifurl{browser: :remote,url:url}else{browser: :chrome}enddriven_by:selenium,using: :headless_chrome,options:optionsend

Now you should get a connection to the remote browser.

$SELENIUM_REMOTE_URL=http://localhost:4444/wd/hubbin/rails test:system

If your application is remote, e.g. within a Docker container, Capybara needsmore input about how tocall remoteservers.

require"test_helper"classApplicationSystemTestCase<ActionDispatch::SystemTestCasesetupdoCapybara.server_host="0.0.0.0"# bind to all interfacesCapybara.app_host="http://#{IPSocket.getaddress(Socket.gethostname)}"ifENV["SELENIUM_REMOTE_URL"].present?end# ...end

Now you should get a connection to a remote browser and server, regardless if itis running in a Docker container or CI.

If your Capybara configuration requires more setup than provided by Rails, thisadditional configuration can be added into theapplication_system_test_case.rbfile.

Please seeCapybara'sdocumentation for additionalsettings.

7.4. Implementing a System Test

This section will demonstrate how to add a system test to your application,which tests a visit to the index page to create a new blog article.

The scaffold generator no longer creates system tests by default. Toinclude system tests when scaffolding, use the--system-tests=true option.Otherwise, create system tests manually for your critical user paths.

$bin/railsgenerate system_test articles

It should have created a test file placeholder. With the output of the previouscommand you should see:

      invoke  test_unit      create    test/system/articles_test.rb

Now, let's open that file and write the first assertion:

require"application_system_test_case"classArticlesTest<ApplicationSystemTestCasetest"viewing the index"dovisitarticles_pathassert_selector"h1",text:"Articles"endend

The test should see that there is anh1 on the articles index page and pass.

Run the system tests.

$bin/rails test:system

By default, runningbin/rails test won't run your system tests. Makesure to runbin/rails test:system to actually run them. You can also runbin/rails test:all to run all tests, including system tests.

7.4.1. Creating Articles System Test

Now you can test the flow for creating a new article.

test"should create Article"dovisitarticles_pathclick_on"New Article"fill_in"Title",with:"Creating an Article"fill_in"Body",with:"Created this article successfully!"click_on"Create Article"assert_text"Creating an Article"end

The first step is to callvisit articles_path. This will take the test to thearticles index page.

Then theclick_on "New Article" will find the "New Article" button on theindex page. This will redirect the browser to/articles/new.

Then the test will fill in the title and body of the article with the specifiedtext. Once the fields are filled in, "Create Article" is clicked on which willsend a POST request to/articles/create.

This redirects the user back to the articles index page, and there it isasserted that the text from the new article's title is on the articles indexpage.

7.4.2. Testing for Multiple Screen Sizes

If you want to test for mobile sizes in addition to testing for desktop, you cancreate another class that inherits fromActionDispatch::SystemTestCase and useit in your test suite. In this example, a file calledmobile_system_test_case.rb is created in the/test directory with thefollowing configuration.

require"test_helper"classMobileSystemTestCase<ActionDispatch::SystemTestCasedriven_by:selenium,using: :chrome,screen_size:[375,667]end

To use this configuration, create a test insidetest/system that inherits fromMobileSystemTestCase. Now you can test your app using multiple differentconfigurations.

require"mobile_system_test_case"classPostsTest<MobileSystemTestCasetest"visiting the index"dovisitposts_urlassert_selector"h1",text:"Posts"endend

7.4.3. Capybara Assertions

Here's an extract of the assertions provided byCapybarawhich can be used in system tests.

AssertionPurpose
assert_button(locator = nil, **options, &optional_filter_block)Checks if the page has a button with the given text, value or id.
assert_current_path(string, **options)Asserts that the page has the given path.
assert_field(locator = nil, **options, &optional_filter_block)Checks if the page has a form field with the given label, name or id.
assert_link(locator = nil, **options, &optional_filter_block)Checks if the page has a link with the given text or id.
assert_selector(*args, &optional_filter_block)Asserts that a given selector is on the page.
assert_table(locator = nil, **options, &optional_filter_blockChecks if the page has a table with the given id or caption.
assert_text(type, text, **options)Asserts that the page has the given text content.

7.4.4. Screenshot Helper

TheScreenshotHelperis a helper designed to capture screenshots of your tests. This can be helpfulfor viewing the browser at the point a test failed, or to view screenshots laterfor debugging.

Two methods are provided:take_screenshot andtake_failed_screenshot.take_failed_screenshot is automatically included inbefore_teardown insideRails.

Thetake_screenshot helper method can be included anywhere in your tests totake a screenshot of the browser.

7.4.5. Taking It Further

System testing is similar tointegration testing in thatit tests the user's interaction with your controller, model, and view, butsystem testing tests your application as if a real user were using it. Withsystem tests, you can test anything that a user would do in your applicationsuch as commenting, deleting articles, publishing draft articles, etc.

8. Test Helpers

To avoid code duplication, you can add your own test helpers. Here is an examplefor signing in:

# test/test_helper.rbmoduleSignInHelperdefsign_in_as(user)postsign_in_url(email:user.email,password:user.password)endendclassActionDispatch::IntegrationTestincludeSignInHelperend
require"test_helper"classProfileControllerTest<ActionDispatch::IntegrationTesttest"should show profile"do# helper is now reusable from any controller test casesign_in_asusers(:david)getprofile_urlassert_response:successendend

8.1. Using Separate Files

If you find your helpers are clutteringtest_helper.rb, you can extract theminto separate files. A good place to store them istest/lib ortest/test_helpers.

# test/test_helpers/multiple_assertions.rbmoduleMultipleAssertionsdefassert_multiple_of_forty_two(number)assert(number%42==0),"expected#{number} to be a multiple of 42"endend

These helpers can then be explicitly required and included as needed:

require"test_helper"require"test_helpers/multiple_assertions"classNumberTest<ActiveSupport::TestCaseincludeMultipleAssertionstest"420 is a multiple of 42"doassert_multiple_of_forty_two420endend

They can also continue to be included directly into the relevant parent classes:

# test/test_helper.rbrequire"test_helpers/sign_in_helper"classActionDispatch::IntegrationTestincludeSignInHelperend

8.2. Eagerly Requiring Helpers

You may find it convenient to eagerly require helpers intest_helper.rb soyour test files have implicit access to them. This can be accomplished usingglobbing, as follows

# test/test_helper.rbDir[Rails.root.join("test","test_helpers","**","*.rb")].each{|file|requirefile}

This has the downside of increasing the boot-up time, as opposed to manuallyrequiring only the necessary files in your individual tests.

9. Testing Routes

Like everything else in your Rails application, you can test your routes. Routetests are stored intest/controllers/ or are part of controller tests. If yourapplication has complex routes, Rails provides a number of useful helpers totest them.

For more information on routing assertions available in Rails, see the APIdocumentation forActionDispatch::Assertions::RoutingAssertions.

10. Testing Views

Testing the response to your request by asserting the presence of key HTMLelements and their content is one way to test the views of your application.Like route tests, view tests are stored intest/controllers/ or are part ofcontroller tests.

10.1. Querying the HTML

Methods likeassert_dom andassert_dom_equal allow you to query HTMLelements of the response by using a simple yet powerful syntax.

assert_dom is an assertion that will return true if matching elements arefound. For example, you could verify that the page title is "Welcome to theRails Testing Guide" as follows:

assert_dom"title","Welcome to the Rails Testing Guide"

You can also use nestedassert_dom blocks for deeper investigation.

In the following example, the innerassert_dom forli.menu_item runs withinthe collection of elements selected by the outer block:

assert_dom"ul.navigation"doassert_dom"li.menu_item"end

A collection of selected elements may also be iterated through so thatassert_dom may be called separately for each element. For example, if theresponse contains two ordered lists, each with four nested list elements thenthe following tests will both pass.

assert_dom"ol"do|elements|elements.eachdo|element|assert_domelement,"li",4endendassert_dom"ol"doassert_dom"li",8end

Theassert_dom_equal method compares two HTML strings to see if they areequal:

assert_dom_equal'<a href="http://www.further-reading.com">Read more</a>',link_to("Read more","http://www.further-reading.com")

For more advanced usage, refer to therails-dom-testingdocumentation.

In order to integrate withrails-dom-testing, tests that inherit fromActionView::TestCase declare adocument_root_element method that returns therendered content as an instance of aNokogiri::XML::Node:

test"renders a link to itself"doarticle=Article.create!title:"Hello, world"render"articles/article",article:articleanchor=document_root_element.at("a")assert_equalarticle.name,anchor.textassert_equalarticle_url(article),anchor["href"]end

If your application depends onNokogiri >=1.14.0 orhigher, andminitest >=5.18.0,document_root_element supportsRuby's PatternMatching:

test"renders a link to itself"doarticle=Article.create!title:"Hello, world"render"articles/article",article:articleanchor=document_root_element.at("a")url=article_url(article)assert_patterndoanchor=>{content:"Hello, world",attributes:[{name:"href",value:url}]}endend

If you'd like to access the sameCapybara-poweredAssertionsthat yourSystem Testing tests utilize, you can define a baseclass that inherits fromActionView::TestCase and transforms thedocument_root_element into apage method:

# test/view_partial_test_case.rbrequire"test_helper"require"capybara/minitest"classViewPartialTestCase<ActionView::TestCaseincludeCapybara::Minitest::AssertionsdefpageCapybara.string(rendered)endend# test/views/article_partial_test.rbrequire"view_partial_test_case"classArticlePartialTest<ViewPartialTestCasetest"renders a link to itself"doarticle=Article.create!title:"Hello, world"render"articles/article",article:articleassert_linkarticle.title,href:article_url(article)endend

More information about the assertions included by Capybara can be found in theCapybara Assertions section.

10.2. Parsing View Content

Starting in Action View version 7.1, therendered helper method returns anobject capable of parsing the view partial's rendered content.

To transform theString content returned by therendered method into anobject, define a parser by callingregister_parser.Callingregister_parser :rss defines arendered.rss helper method. Forexample, to parse renderedRSS content into an object withrendered.rss,register a call toRSS::Parser.parse:

register_parser:rss,->rendered{RSS::Parser.parse(rendered)}test"renders RSS"doarticle=Article.create!(title:"Hello, world")renderformats: :rss,partial:articleassert_equal"Hello, world",rendered.rss.items.last.titleend

By default,ActionView::TestCase defines a parser for:

test"renders HTML"doarticle=Article.create!(title:"Hello, world")renderpartial:"articles/article",locals:{article:article}assert_pattern{rendered.html.at("main h1")=>{content:"Hello, world"}}endtest"renders JSON"doarticle=Article.create!(title:"Hello, world")renderformats: :json,partial:"articles/article",locals:{article:article}assert_pattern{rendered.json=>{title:"Hello, world"}}end

10.3. Additional View-Based Assertions

There are more assertions that are primarily used in testing views:

AssertionPurpose
assert_dom_emailAllows you to make assertions on the body of an e-mail.
assert_dom_encodedAllows you to make assertions on encoded HTML. It does this by un-encoding the contents of each element and then calling the block with all the un-encoded elements.
css_select(selector) orcss_select(element, selector)Returns an array of all the elements selected by theselector. In the second variant it first matches the baseelement and tries to match theselector expression on any of its children. If there are no matches both variants return an empty array.

Here's an example of usingassert_dom_email:

assert_dom_emaildoassert_dom"small","Please click the 'Unsubscribe' link if you want to opt-out."end

10.4. Testing View Partials

Partial templates - usually called"partials" - can break the rendering process into more manageable chunks. Withpartials, you can extract sections of code from your views to separate files andreuse them in multiple places.

View tests provide an opportunity to test that partials render content the wayyou expect. View partial tests can be stored intest/views/ and inherit fromActionView::TestCase.

To render a partial, callrender like you would in a template. The content isavailable through the test-localrendered method:

classArticlePartialTest<ActionView::TestCasetest"renders a link to itself"doarticle=Article.create!title:"Hello, world"render"articles/article",article:articleassert_includesrendered,article.titleendend

Tests that inherit fromActionView::TestCase also have access toassert_dom and theother additional view-basedassertions provided byrails-dom-testing:

test"renders a link to itself"doarticle=Article.create!title:"Hello, world"render"articles/article",article:articleassert_dom"a[href=?]",article_url(article),text:article.titleend

10.5. Testing View Helpers

A helper is a module where you can define methods which are available in yourviews.

In order to test helpers, all you need to do is check that the output of thehelper method matches what you'd expect. Tests related to the helpers arelocated under thetest/helpers directory.

Given we have the following helper:

moduleUsersHelperdeflink_to_user(user)link_to"#{user.first_name}#{user.last_name}",userendend

We can test the output of this method like this:

classUsersHelperTest<ActionView::TestCasetest"should return the user's full name"douser=users(:david)assert_dom_equal%{<a href="/user/#{user.id}">David Heinemeier Hansson</a>},link_to_user(user)endend

Moreover, since the test class extends fromActionView::TestCase, you haveaccess to Rails' helper methods such aslink_to orpluralize.

11. Testing Mailers

Your mailer classes - like every other part of your Rails application - shouldbe tested to ensure that they are working as expected.

The goals of testing your mailer classes are to ensure that:

  • emails are being processed (created and sent)
  • the email content is correct (subject, sender, body, etc)
  • the right emails are being sent at the right times

There are two aspects of testing your mailer, the unit tests and the functionaltests. In the unit tests, you run the mailer in isolation with tightlycontrolled inputs and compare the output to a known value (afixture). In the functional tests you don't so much test thedetails produced by the mailer; instead, you test that the controllers andmodels are using the mailer in the right way. You test to prove that the rightemail was sent at the right time.

11.1. Unit Testing

In order to test that your mailer is working as expected, you can use unit teststo compare the actual results of the mailer with pre-written examples of whatshould be produced.

11.1.1. Mailer Fixtures

For the purposes of unit testing a mailer, fixtures are used to provide anexample of how the outputshould look. Because these are example emails, andnot Active Record data like the other fixtures, they are kept in their ownsubdirectory apart from the other fixtures. The name of the directory withintest/fixtures directly corresponds to the name of the mailer. So, for a mailernamedUserMailer, the fixtures should reside intest/fixtures/user_mailerdirectory.

If you generated your mailer, the generator does not create stub fixtures forthe mailers actions. You'll have to create those files yourself as describedabove.

11.1.2. The Basic Test Case

Here's a unit test to test a mailer namedUserMailer whose actioninvite isused to send an invitation to a friend:

require"test_helper"classUserMailerTest<ActionMailer::TestCasetest"invite"do# Create the email and store it for further assertionsemail=UserMailer.create_invite("me@example.com","friend@example.com",Time.now)# Send the email, then test that it got queuedassert_emails1doemail.deliver_nowend# Test the body of the sent email contains what we expect it toassert_equal["me@example.com"],email.fromassert_equal["friend@example.com"],email.toassert_equal"You have been invited by me@example.com",email.subjectassert_equalread_fixture("invite").join,email.body.to_sendend

In the test the email is created and the returned object is stored in theemail variable. The first assert checks it was sent, then, in the second batchof assertions, the email contents are checked. The helperread_fixture is usedto read in the content from this file.

email.body.to_s is present when there's only one (HTML or text) partpresent. If the mailer provides both, you can test your fixture against specificparts withemail.text_part.body.to_s oremail.html_part.body.to_s.

Here's the content of theinvite fixture:

Hi friend@example.com,You have been invited.Cheers!

11.1.3. Configuring the Delivery Method for Test

The lineActionMailer::Base.delivery_method = :test inconfig/environments/test.rb sets the delivery method to test mode so that theemail will not actually be delivered (useful to avoid spamming your users whiletesting). Instead, the email will be appended to an array(ActionMailer::Base.deliveries).

TheActionMailer::Base.deliveries array is only reset automatically inActionMailer::TestCase andActionDispatch::IntegrationTest tests. If youwant to have a clean slate outside these test cases, you can reset it manuallywith:ActionMailer::Base.deliveries.clear

11.1.4. Testing Enqueued Emails

You can use theassert_enqueued_email_with assertion to confirm that the emailhas been enqueued with all of the expected mailer method arguments and/orparameterized mailer parameters. This allows you to match any emails that havebeen enqueued with thedeliver_later method.

As with the basic test case, we create the email and store the returned objectin theemail variable. The following examples include variations of passingarguments and/or parameters.

This example will assert that the email has been enqueued with the correctarguments:

require"test_helper"classUserMailerTest<ActionMailer::TestCasetest"invite"do# Create the email and store it for further assertionsemail=UserMailer.create_invite("me@example.com","friend@example.com")# Test that the email got enqueued with the correct argumentsassert_enqueued_email_withUserMailer,:create_invite,args:["me@example.com","friend@example.com"]doemail.deliver_laterendendend

This example will assert that a mailer has been enqueued with the correct mailermethod named arguments by passing a hash of the arguments asargs:

require"test_helper"classUserMailerTest<ActionMailer::TestCasetest"invite"do# Create the email and store it for further assertionsemail=UserMailer.create_invite(from:"me@example.com",to:"friend@example.com")# Test that the email got enqueued with the correct named argumentsassert_enqueued_email_withUserMailer,:create_invite,args:[{from:"me@example.com",to:"friend@example.com"}]doemail.deliver_laterendendend

This example will assert that a parameterized mailer has been enqueued with thecorrect parameters and arguments. The mailer parameters are passed asparamsand the mailer method arguments asargs:

require"test_helper"classUserMailerTest<ActionMailer::TestCasetest"invite"do# Create the email and store it for further assertionsemail=UserMailer.with(all:"good").create_invite("me@example.com","friend@example.com")# Test that the email got enqueued with the correct mailer parameters and argumentsassert_enqueued_email_withUserMailer,:create_invite,params:{all:"good"},args:["me@example.com","friend@example.com"]doemail.deliver_laterendendend

This example shows an alternative way to test that a parameterized mailer hasbeen enqueued with the correct parameters:

require"test_helper"classUserMailerTest<ActionMailer::TestCasetest"invite"do# Create the email and store it for further assertionsemail=UserMailer.with(to:"friend@example.com").create_invite# Test that the email got enqueued with the correct mailer parametersassert_enqueued_email_withUserMailer.with(to:"friend@example.com"),:create_invitedoemail.deliver_laterendendend

11.2. Functional and System Testing

Unit testing allows us to test the attributes of the email while functional andsystem testing allows us to test whether user interactions appropriately triggerthe email to be delivered. For example, you can check that the invite friendoperation is sending an email appropriately:

# Integration Testrequire"test_helper"classUsersControllerTest<ActionDispatch::IntegrationTesttest"invite friend"do# Asserts the difference in the ActionMailer::Base.deliveriesassert_emails1dopostinvite_friend_url,params:{email:"friend@example.com"}endendend
# System Testrequire"test_helper"classUsersTest<ActionDispatch::SystemTestCasedriven_by:selenium,using: :headless_chrometest"inviting a friend"dovisitinvite_users_urlfill_in"Email",with:"friend@example.com"assert_emails1doclick_on"Invite"endendend

Theassert_emails method is not tied to a particular deliver method andwill work with emails delivered with either thedeliver_now ordeliver_latermethod. If we explicitly want to assert that the email has been enqueued we canuse theassert_enqueued_email_with (examplesabove) orassert_enqueued_emails methods. Moreinformation can be found in thedocumentation.

12. Testing Jobs

Jobs can be tested in isolation (focusing on the job's behavior) and in context(focusing on the calling code's behavior).

12.1. Testing Jobs in Isolation

When you generate a job, an associated test file will also be generated in thetest/jobs directory.

Here is a test you could write for a billing job:

require"test_helper"classBillingJobTest<ActiveJob::TestCasetest"account is charged"doperform_enqueued_jobsdoBillingJob.perform_later(account,product)endassertaccount.reload.charged_for?(product)endend

The default queue adapter for tests will not perform jobs untilperform_enqueued_jobs is called. Additionally, it will clear all jobsbefore each test is run so that tests do not interfere with each other.

The test usesperform_enqueued_jobs andperform_later instead ofperform_now so that if retries are configured, retry failures are caughtby the test instead of being re-enqueued and ignored.

12.2. Testing Jobs in Context

It's good practice to test that jobs are correctly enqueued, for example, by acontroller action. TheActiveJob::TestHelper module provides severalmethods that can help with this, such asassert_enqueued_with.

Here is an example that tests an account model method:

require"test_helper"classAccountTest<ActiveSupport::TestCaseincludeActiveJob::TestHelpertest"#charge_for enqueues billing job"doassert_enqueued_with(job:BillingJob)doaccount.charge_for(product)endassert_notaccount.reload.charged_for?(product)perform_enqueued_jobsassertaccount.reload.charged_for?(product)endend

12.3. Testing that Exceptions are Raised

Testing that your job raises an exception in certain cases can be tricky,especially when you have retries configured. Theperform_enqueued_jobs helperfails any test where a job raises an exception, so to have the test succeed whenthe exception is raised you have to call the job'sperform method directly.

require"test_helper"classBillingJobTest<ActiveJob::TestCasetest"does not charge accounts with insufficient funds"doassert_raises(InsufficientFundsError)doBillingJob.new(empty_account,product).performendassert_notaccount.reload.charged_for?(product)endend

This method is not recommended in general, as it circumvents some parts of theframework, such as argument serialization.

13. Testing Action Cable

Since Action Cable is used at different levels inside your application, you'llneed to test both the channels, connection classes themselves, and that otherentities broadcast correct messages.

13.1. Connection Test Case

By default, when you generate a new Rails application with Action Cable, a testfor the base connection class (ApplicationCable::Connection) is generated aswell undertest/channels/application_cable directory.

Connection tests aim to check whether a connection's identifiers get assignedproperly or that any improper connection requests are rejected. Here is anexample:

classApplicationCable::ConnectionTest<ActionCable::Connection::TestCasetest"connects with params"do# Simulate a connection opening by calling the `connect` methodconnectparams:{user_id:42}# You can access the Connection object via `connection` in testsassert_equalconnection.user_id,"42"endtest"rejects connection without params"do# Use `assert_reject_connection` matcher to verify that# connection is rejectedassert_reject_connection{connect}endend

You can also specify request cookies the same way you do in integration tests:

test"connects with cookies"docookies.signed[:user_id]="42"connectassert_equalconnection.user_id,"42"end

See the API documentation forActionCable::Connection::TestCasefor more information.

13.2. Channel Test Case

By default, when you generate a channel, an associated test will be generated aswell under thetest/channels directory. Here's an example test with a chatchannel:

require"test_helper"classChatChannelTest<ActionCable::Channel::TestCasetest"subscribes and stream for room"do# Simulate a subscription creation by calling `subscribe`subscriberoom:"15"# You can access the Channel object via `subscription` in testsassertsubscription.confirmed?assert_has_stream"chat_15"endend

This test is pretty simple and only asserts that the channel subscribes theconnection to a particular stream.

You can also specify the underlying connection identifiers. Here's an exampletest with a web notifications channel:

require"test_helper"classWebNotificationsChannelTest<ActionCable::Channel::TestCasetest"subscribes and stream for user"dostub_connectioncurrent_user:users(:john)subscribeassert_has_stream_forusers(:john)endend

See the API documentation forActionCable::Channel::TestCasefor more information.

13.3. Custom Assertions And Testing Broadcasts Inside Other Components

Action Cable ships with a bunch of custom assertions that can be used to lessenthe verbosity of tests. For a full list of available assertions, see the APIdocumentation forActionCable::TestHelper.

It's a good practice to ensure that the correct message has been broadcastedinside other components (e.g. inside your controllers). This is precisely wherethe custom assertions provided by Action Cable are pretty useful. For instance,within a model:

require"test_helper"classProductTest<ActionCable::TestCasetest"broadcast status after charge"doassert_broadcast_on("products:#{product.id}",type:"charged")doproduct.charge(account)endendend

If you want to test the broadcasting made withChannel.broadcast_to, youshould useChannel.broadcasting_for to generate an underlying stream name:

# app/jobs/chat_relay_job.rbclassChatRelayJob<ApplicationJobdefperform(room,message)ChatChannel.broadcast_toroom,text:messageendend
# test/jobs/chat_relay_job_test.rbrequire"test_helper"classChatRelayJobTest<ActiveJob::TestCaseincludeActionCable::TestHelpertest"broadcast message to room"doroom=rooms(:all)assert_broadcast_on(ChatChannel.broadcasting_for(room),text:"Hi!")doChatRelayJob.perform_now(room,"Hi!")endendend

14. Running tests in Continuous Integration (CI)

Continuous Integration (CI) is a development practice where changes arefrequently integrated into the main codebase, and as such, are automaticallytested before merge.

To run all tests in a CI environment, there's just one command you need:

$bin/rails test

If you are usingSystem Tests,bin/rails test will not runthem, since they can be slow. To also run them, add another CI step that runsbin/rails test:system, or change your first step tobin/rails test:all,which runs all tests including system tests.

15. Parallel Testing

Running tests in parallel reduces the time it takes your entire test suite torun. While forking processes is the default method, threading is supported aswell.

15.1. Parallel Testing with Processes

The default parallelization method is to fork processes using Ruby's DRb system.The processes are forked based on the number of workers provided. The defaultnumber is the actual core count on the machine, but can be changed by the numberpassed to theparallelize method.

To enable parallelization add the following to yourtest_helper.rb:

classActiveSupport::TestCaseparallelize(workers:2)end

The number of workers passed is the number of times the process will be forked.You may want to parallelize your local test suite differently from your CI, soan environment variable is provided to be able to easily change the number ofworkers a test run should use:

$PARALLEL_WORKERS=15bin/rails test

When parallelizing tests, Active Record automatically handles creating adatabase and loading the schema into the database for each process. Thedatabases will be suffixed with the number corresponding to the worker. Forexample, if you have 2 workers the tests will createtest-database-0 andtest-database-1 respectively.

If the number of workers passed is 1 or fewer the processes will not be forkedand the tests will not be parallelized and they will use the originaltest-database database.

Two hooks are provided, one runs when the process is forked, and one runs beforethe forked process is closed. These can be useful if your app uses multipledatabases or performs other tasks that depend on the number of workers.

Theparallelize_setup method is called right after the processes are forked.Theparallelize_teardown method is called right before the processes areclosed.

classActiveSupport::TestCaseparallelize_setupdo|worker|# setup databasesendparallelize_teardowndo|worker|# cleanup databasesendparallelize(workers: :number_of_processors)end

These methods are not needed or available when using parallel testing withthreads.

15.2. Parallel Testing with Threads

If you prefer using threads or are using JRuby, a threaded parallelizationoption is provided. The threaded parallelizer is backed by minitest'sParallel::Executor.

To change the parallelization method to use threads over forks put the followingin yourtest_helper.rb:

classActiveSupport::TestCaseparallelize(workers: :number_of_processors,with: :threads)end

Rails applications generated from JRuby or TruffleRuby will automaticallyinclude thewith: :threads option.

As in the section above, you can also use the environment variablePARALLEL_WORKERS in this context, to change the number of workers your testrun should use.

15.3. Testing Parallel Transactions

When you want to test code that runs parallel database transactions in threads,those can block each other because they are already nested under the implicittest transaction.

To workaround this, you can disable transactions in a test case class by settingself.use_transactional_tests = false:

classWorkerTest<ActiveSupport::TestCaseself.use_transactional_tests=falsetest"parallel transactions"do# start some threads that create transactionsendend

With disabled transactional tests, you have to clean up any data testscreate as changes are not automatically rolled back after the test completes.

15.4. Threshold to Parallelize tests

Running tests in parallel adds an overhead in terms of database setup andfixture loading. Because of this, Rails won't parallelize executions thatinvolve fewer than 50 tests.

You can configure this threshold in yourtest.rb:

config.active_support.test_parallelization_threshold=100

And also when setting up parallelization at the test case level:

classActiveSupport::TestCaseparallelizethreshold:100end

16. Testing Eager Loading

Normally, applications do not eager load in thedevelopment ortestenvironments to speed things up. But they do in theproduction environment.

If some file in the project cannot be loaded for whatever reason, it isimportant to detect it before deploying to production.

16.1. Continuous Integration

If your project has CI in place, eager loading in CI is an easy way to ensurethe application eager loads.

CIs typically set an environment variable to indicate the test suite is runningthere. For example, it could beCI:

# config/environments/test.rbconfig.eager_load=ENV["CI"].present?

Starting with Rails 7, newly generated applications are configured that way bydefault.

If your project does not have continuous integration, you can still eager loadin the test suite by callingRails.application.eager_load!:

require"test_helper"classZeitwerkComplianceTest<ActiveSupport::TestCasetest"eager loads all files without errors"doassert_nothing_raised{Rails.application.eager_load!}endend

17. Additional Testing Resources

17.1. Errors

In system tests, integration tests and functional controller tests, Rails willattempt to rescue from errors raised and respond with HTML error pages bydefault. This behavior can be controlled by theconfig.action_dispatch.show_exceptionsconfiguration.

17.2. Testing Time-Dependent Code

Rails provides built-in helper methods that enable you to assert that yourtime-sensitive code works as expected.

The following example uses thetravel_to helper:

# Given a user is eligible for gifting a month after they register.user=User.create(name:"Gaurish",activation_date:Date.new(2004,10,24))assert_notuser.applicable_for_gifting?travel_toDate.new(2004,11,24)do# Inside the `travel_to` block `Date.current` is stubbedassert_equalDate.new(2004,10,24),user.activation_dateassertuser.applicable_for_gifting?end# The change was visible only inside the `travel_to` block.assert_equalDate.new(2004,10,24),user.activation_date

Please seeActiveSupport::Testing::TimeHelpers APIreference for more information about the available time helpers.



Back to top
[8]ページ先頭

©2009-2025 Movatter.jp