diff options
Diffstat (limited to 'guides/source/testing.md')
-rw-r--r-- | guides/source/testing.md | 372 |
1 files changed, 227 insertions, 145 deletions
diff --git a/guides/source/testing.md b/guides/source/testing.md index 7747318d32..edf4813d74 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -1,8 +1,7 @@ A Guide to Testing Rails Applications ===================================== -This guide covers built-in mechanisms offered by Rails to test your -application. +This guide covers built-in mechanisms in Rails for testing your application. After reading this guide, you will know: @@ -38,11 +37,11 @@ Rails creates a `test` folder for you as soon as you create a Rails project usin ```bash $ ls -F test - -fixtures/ functional/ integration/ performance/ test_helper.rb unit/ +controllers/ helpers/ mailers/ test_helper.rb +fixtures/ integration/ models/ ``` -The `unit` directory is meant to hold tests for your models, the `functional` directory is meant to hold tests for your controllers, the `integration` directory is meant to hold tests that involve any number of controllers interacting, and the `performance` directory is meant for performance tests. +The `models` directory is meant to hold tests for your models, the `controllers` directory is meant to hold tests for your controllers and the `integration` directory is meant to hold tests that involve any number of controllers interacting. Fixtures are a way of organizing test data; they reside in the `fixtures` folder. @@ -65,20 +64,36 @@ YAML-formatted fixtures are a very human-friendly way to describe your sample da Here's a sample YAML fixture file: ```yaml -# lo & behold! I am a YAML comment! +# lo & behold! I am a YAML comment! david: - name: David Heinemeier Hansson - birthday: 1979-10-15 - profession: Systems development + name: David Heinemeier Hansson + birthday: 1979-10-15 + profession: Systems development steve: - name: Steve Ross Kellock - birthday: 1974-09-27 - profession: guy with keyboard + name: Steve Ross Kellock + birthday: 1974-09-27 + profession: guy with keyboard ``` Each fixture is given a name followed by an indented list of colon-separated key/value pairs. Records are typically separated by a blank space. You can place comments in a fixture file by using the # character in the first column. Keys which resemble YAML keywords such as 'yes' and 'no' are quoted so that the YAML Parser correctly interprets them. +If you are working with [associations](/association_basics.html), you can simply +define a reference node between two different fixtures. Here's an example with +a belongs_to/has_many association: + +```yaml +# In fixtures/categories.yml +about: + name: About + +# In fixtures/articles.yml +one: + title: Welcome to Rails! + body: Hello world! + category: about +``` + #### ERB'in It Up ERB allows you to embed Ruby code within templates. The YAML fixture format is pre-processed with ERB when Rails loads fixtures. This allows you to use Ruby to help you generate some sample data. For example, the following code generates a thousand users: @@ -86,14 +101,14 @@ ERB allows you to embed Ruby code within templates. The YAML fixture format is p ```erb <% 1000.times do |n| %> user_<%= n %>: - username: <%= "user%03d" % n %> - email: <%= "user%03d@example.com" % n %> + username: <%= "user#{n}" %> + email: <%= "user#{n}@example.com" %> <% end %> ``` #### Fixtures in Action -Rails by default automatically loads all fixtures from the `test/fixtures` folder for your unit and functional test. Loading involves three steps: +Rails by default automatically loads all fixtures from the `test/fixtures` folder for your models and controllers test. Loading involves three steps: * Remove any existing data from the table corresponding to the fixture * Load the fixture data into the table @@ -117,7 +132,7 @@ email(david.girlfriend.email, david.location_tonight) Unit Testing your Models ------------------------ -In Rails, unit tests are what you write to test your models. +In Rails, models tests are what you write to test your models. For this guide we will be using Rails _scaffolding_. It will create the model, a migration, controller and views for the new resource in a single operation. It will also create a full test suite following Rails best practices. I will be using examples from this generated code and will be supplementing it with additional examples where necessary. @@ -140,10 +155,9 @@ The default test stub in `test/models/post_test.rb` looks like this: require 'test_helper' class PostTest < ActiveSupport::TestCase - # Replace this with your real tests. - test "the truth" do - assert true - end + # test "the truth" do + # assert true + # end end ``` @@ -161,9 +175,10 @@ class PostTest < ActiveSupport::TestCase The `PostTest` class defines a _test case_ because it inherits from `ActiveSupport::TestCase`. `PostTest` thus has all the methods available from `ActiveSupport::TestCase`. You'll see those methods a little later in this guide. -Any method defined within a `Test::Unit` test case that begins with `test` (case sensitive) is simply called a test. So, `test_password`, `test_valid_password` and `testValidPassword` all are legal test names and are run automatically when the test case is run. +Any method defined within a class inherited from `MiniTest::Unit::TestCase` +(which is the superclass of `ActiveSupport::TestCase`) that begins with `test` (case sensitive) is simply called a test. So, `test_password`, `test_valid_password` and `testValidPassword` all are legal test names and are run automatically when the test case is run. -Rails adds a `test` method that takes a test name and a block. It generates a normal `Test::Unit` test with method names prefixed with `test_`. So, +Rails adds a `test` method that takes a test name and a block. It generates a normal `MiniTest::Unit` test with method names prefixed with `test_`. So, ```ruby test "the truth" do @@ -224,34 +239,30 @@ TIP: You can see all these rake tasks and their descriptions by running `rake -- ### Running Tests -Running a test is as simple as invoking the file containing the test cases through Ruby: +Running a test is as simple as invoking the file containing the test cases through `rake test` command. ```bash -$ ruby -Itest test/models/post_test.rb - -Loaded suite models/post_test -Started +$ rake test test/models/post_test.rb . -Finished in 0.023513 seconds. -1 tests, 1 assertions, 0 failures, 0 errors -``` +Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s. -This will run all the test methods from the test case. Note that `test_helper.rb` is in the `test` directory, hence this directory needs to be added to the load path using the `-I` switch. +1 tests, 1 assertions, 0 failures, 0 errors, 0 skips +``` -You can also run a particular test method from the test case by using the `-n` switch with the `test method name`. +You can also run a particular test method from the test case by running the test and providing the `test method name`. ```bash -$ ruby -Itest test/models/post_test.rb -n test_the_truth - -Loaded suite models/post_test -Started +$ rake test test/models/post_test.rb test_the_truth . -Finished in 0.023513 seconds. -1 tests, 1 assertions, 0 failures, 0 errors +Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s. + +1 tests, 1 assertions, 0 failures, 0 errors, 0 skips ``` +This will run all test methods from the test case. Note that `test_helper.rb` is in the `test` directory, hence this directory needs to be added to the load path using the `-I` switch. + The `.` (dot) above indicates a passing test. When a test fails you see an `F`; when a test throws an error you see an `E` in its place. The last line of the output is the summary. To see how a test failure is reported, you can add a failing test to the `post_test.rb` test case. @@ -266,17 +277,16 @@ end Let us run this newly added test. ```bash -$ ruby unit/post_test.rb -n test_should_not_save_post_without_title -Loaded suite -e -Started +$ rake test test/models/post_test.rb test_should_not_save_post_without_title F -Finished in 0.102072 seconds. + +Finished tests in 0.044632s, 22.4054 tests/s, 22.4054 assertions/s. 1) Failure: -test_should_not_save_post_without_title(PostTest) [/test/models/post_test.rb:6]: -<false> is not true. +test_should_not_save_post_without_title(PostTest) [test/models/post_test.rb:6]: +Failed assertion, no message given. -1 tests, 1 assertions, 1 failures, 0 errors +1 tests, 1 assertions, 1 failures, 0 errors, 0 skips ``` In the output, `F` denotes a failure. You can see the corresponding trace shown under `1)` along with the name of the failing test. The next few lines contain the stack trace followed by a message which mentions the actual value and the expected value by the assertion. The default assertion messages provide just enough information to help pinpoint the error. To make the assertion failure message more readable, every assertion provides an optional message parameter, as shown here: @@ -292,9 +302,8 @@ Running this test shows the friendlier assertion message: ```bash 1) Failure: -test_should_not_save_post_without_title(PostTest) [/test/models/post_test.rb:6]: -Saved the post without a title. -<false> is not true. +test_should_not_save_post_without_title(PostTest) [test/models/post_test.rb:6]: +Saved the post without a title ``` Now to get this test to pass we can add a model level validation for the _title_ field. @@ -308,13 +317,12 @@ end Now the test should pass. Let us verify by running the test again: ```bash -$ ruby unit/post_test.rb -n test_should_not_save_post_without_title -Loaded suite unit/post_test -Started +$ rake test test/models/post_test.rb test_should_not_save_post_without_title . -Finished in 0.193608 seconds. -1 tests, 1 assertions, 0 failures, 0 errors +Finished tests in 0.047721s, 20.9551 tests/s, 20.9551 assertions/s. + +1 tests, 1 assertions, 0 failures, 0 errors, 0 skips ``` Now, if you noticed, we first wrote a test which fails for a desired functionality, then we wrote some code which adds the functionality and finally we ensured that our test passes. This approach to software development is referred to as _Test-Driven Development_ (TDD). @@ -334,18 +342,17 @@ end Now you can see even more output in the console from running the tests: ```bash -$ ruby unit/post_test.rb -n test_should_report_error -Loaded suite -e -Started +$ rake test test/models/post_test.rb test_should_report_error E -Finished in 0.082603 seconds. + +Finished tests in 0.030974s, 32.2851 tests/s, 0.0000 assertions/s. 1) Error: test_should_report_error(PostTest): -NameError: undefined local variable or method `some_undefined_variable' for #<PostTest:0x249d354> - /test/models/post_test.rb:6:in `test_should_report_error' +NameError: undefined local variable or method `some_undefined_variable' for #<PostTest:0x007fe32e24afe0> + test/models/post_test.rb:10:in `block in <class:PostTest>' -1 tests, 0 assertions, 0 failures, 1 errors +1 tests, 0 assertions, 0 failures, 1 errors, 0 skips ``` Notice the 'E' in the output. It denotes a test with error. @@ -356,31 +363,38 @@ NOTE: The execution of each test method stops as soon as any error or an asserti Ideally, you would like to include a test for everything which could possibly break. It's a good practice to have at least one test for each of your validations and at least one test for every method in your model. -### Assertions Available +### Available Assertions By now you've caught a glimpse of some of the assertions that are available. Assertions are the worker bees of testing. They are the ones that actually perform the checks to ensure that things are going as planned. -There are a bunch of different types of assertions you can use. Here's the complete list of assertions that ship with `test/unit`, the default testing library used by Rails. The `[msg]` parameter is an optional string message you can specify to make your test failure messages clearer. It's not required. +There are a bunch of different types of assertions you can use. +Here's an extract of the assertions you can use with `minitest`, the default testing library used by Rails. The `[msg]` parameter is an optional string message you can specify to make your test failure messages clearer. It's not required. | Assertion | Purpose | | ---------------------------------------------------------------- | ------- | -| `assert( boolean, [msg] )` | Ensures that the object/expression is true.| +| `assert( test, [msg] )` | Ensures that `test` is true.| +| `assert_not( test, [msg] )` | Ensures that `test` is false.| | `assert_equal( expected, actual, [msg] )` | Ensures that `expected == actual` is true.| | `assert_not_equal( expected, actual, [msg] )` | Ensures that `expected != actual` is true.| | `assert_same( expected, actual, [msg] )` | Ensures that `expected.equal?(actual)` is true.| -| `assert_not_same( expected, actual, [msg] )` | Ensures that `!expected.equal?(actual)` is true.| +| `assert_not_same( expected, actual, [msg] )` | Ensures that `expected.equal?(actual)` is false.| | `assert_nil( obj, [msg] )` | Ensures that `obj.nil?` is true.| -| `assert_not_nil( obj, [msg] )` | Ensures that `!obj.nil?` is true.| +| `assert_not_nil( obj, [msg] )` | Ensures that `obj.nil?` is false.| | `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_in_delta( expecting, actual, delta, [msg] )` | Ensures that the numbers `expecting` and `actual` are within `delta` of each other.| +| `assert_in_delta( expecting, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are within `delta` of each other.| +| `assert_not_in_delta( expecting, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are not within `delta` of each other.| | `assert_throws( symbol, [msg] ) { block }` | Ensures that the given block throws the symbol.| -| `assert_raise( exception1, exception2, ... ) { block }` | Ensures that the given block raises one of the given exceptions.| +| `assert_raises( exception1, exception2, ... ) { block }` | Ensures that the given block raises one of the given exceptions.| | `assert_nothing_raised( exception1, exception2, ... ) { block }` | Ensures that the given block doesn't raise one of the given exceptions.| -| `assert_instance_of( class, obj, [msg] )` | Ensures that `obj` is of the `class` type.| +| `assert_instance_of( class, obj, [msg] )` | Ensures that `obj` is an instance of `class`.| +| `assert_not_instance_of( class, obj, [msg] )` | Ensures that `obj` is not an instance of `class`.| | `assert_kind_of( class, obj, [msg] )` | Ensures that `obj` is or descends from `class`.| -| `assert_respond_to( obj, symbol, [msg] )` | Ensures that `obj` has a method called `symbol`.| -| `assert_operator( obj1, operator, obj2, [msg] )` | Ensures that `obj1.operator(obj2)` is true.| +| `assert_not_kind_of( class, obj, [msg] )` | Ensures that `obj` is not an instance of `class` and is not descending from it.| +| `assert_respond_to( obj, symbol, [msg] )` | Ensures that `obj` responds to `symbol`.| +| `assert_not_respond_to( obj, symbol, [msg] )` | Ensures that `obj` does not respond to `symbol`.| +| `assert_operator( obj1, operator, [obj2], [msg] )` | Ensures that `obj1.operator(obj2)` is true.| +| `assert_not_operator( obj1, operator, [obj2], [msg] )` | Ensures that `obj1.operator(obj2)` is false.| | `assert_send( array, [msg] )` | Ensures that executing the method listed in `array[1]` on the object in `array[0]` with the parameters of `array[2 and up]` is true. This one is weird eh?| | `flunk( [msg] )` | Ensures failure. This is useful to explicitly mark a test that isn't finished yet.| @@ -398,7 +412,7 @@ Rails adds some custom assertions of its own to the `test/unit` framework: | `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_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 extras 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_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| +| `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| | `assert_redirected_to(options = {}, message=nil)` | Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial, such that `assert_redirected_to(controller: "weblog")` will also match the redirection of `redirect_to(controller: "weblog", action: "show")` and so on.| | `assert_template(expected = nil, message=nil)` | Asserts that the request was rendered with the appropriate template file.| @@ -424,10 +438,12 @@ Now that we have used Rails scaffold generator for our `Post` resource, it has a Let me take you through one such test, `test_should_get_index` from the file `posts_controller_test.rb`. ```ruby -test "should get index" do - get :index - assert_response :success - assert_not_nil assigns(:posts) +class PostsControllerTest < ActionController::TestCase + test "should get index" do + get :index + assert_response :success + assert_not_nil assigns(:posts) + end end ``` @@ -485,7 +501,7 @@ NOTE: Functional tests do not verify whether the specified request type should b ### The Four Hashes of the Apocalypse -After a request has been made by using one of the 5 methods (`get`, `post`, etc.) and processed, you will have 4 Hash objects ready for use: +After a request has been made using one of the 6 methods (`get`, `post`, etc.) and processed, you will have 4 Hash objects ready for use: * `assigns` - Any objects that are stored as instance variables in actions for use in views. * `cookies` - Any cookies that are set. @@ -511,6 +527,21 @@ You also have access to three instance variables in your functional tests: * `@request` - The request * `@response` - The response +### Setting Headers and CGI variables + +Headers and cgi variables can be set directly on the `@request` +instance variable: + +```ruby +# setting a HTTP Header +@request.headers["Accept"] = "text/plain, text/html" +get :index # simulate the request with custom header + +# setting a CGI variable +@request.headers["HTTP_REFERER"] = "http://example.com/home" +post :create # simulate the request with custom env variable +``` + ### Testing Templates and Layouts If you want to make sure that the response rendered the correct template and layout, you can use the `assert_template` @@ -609,11 +640,11 @@ The `assert_select` assertion is quite powerful. For more advanced usage, refer There are more assertions that are primarily used in testing views: -| Assertion | Purpose | -| ---------------------------------------------------------- | ------- | -| `assert_select_email` | Allows you to make assertions on the body of an e-mail. | -| `assert_select_encoded` | Allows 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)` or `css_select(element, selector)` | Returns an array of all the elements selected by the _selector_. In the second variant it first matches the base _element_ and tries to match the _selector_ expression on any of its children. If there are no matches both variants return an empty array.| +| Assertion | Purpose | +| --------------------------------------------------------- | ------- | +| `assert_select_email` | Allows you to make assertions on the body of an e-mail. | +| `assert_select_encoded` | Allows 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)` or `css_select(element, selector)` | Returns an array of all the elements selected by the _selector_. In the second variant it first matches the base _element_ and tries to match the _selector_ expression on any of its children. If there are no matches both variants return an empty array.| Here's an example of using `assert_select_email`: @@ -642,12 +673,9 @@ Here's what a freshly-generated integration test looks like: require 'test_helper' class UserFlowsTest < ActionDispatch::IntegrationTest - fixtures :all - - # Replace this with your real tests. - test "the truth" do - assert true - end + # test "the truth" do + # assert true + # end end ``` @@ -688,9 +716,9 @@ class UserFlowsTest < ActionDispatch::IntegrationTest get "/login" assert_response :success - post_via_redirect "/login", username: users(:avs).username, password: users(:avs).password + post_via_redirect "/login", username: users(:david).username, password: users(:david).password assert_equal '/welcome', path - assert_equal 'Welcome avs!', flash[:notice] + assert_equal 'Welcome david!', flash[:notice] https!(false) get "/posts/all" @@ -712,17 +740,17 @@ class UserFlowsTest < ActionDispatch::IntegrationTest test "login and browse site" do - # User avs logs in - avs = login(:avs) + # User david logs in + david = login(:david) # User guest logs in guest = login(:guest) # Both are now available in different sessions - assert_equal 'Welcome avs!', avs.flash[:notice] + assert_equal 'Welcome david!', david.flash[:notice] assert_equal 'Welcome guest!', guest.flash[:notice] - # User avs can browse site - avs.browses_site + # User david can browse site + david.browses_site # User guest can browse site as well guest.browses_site @@ -731,55 +759,54 @@ class UserFlowsTest < ActionDispatch::IntegrationTest private - module CustomDsl - def browses_site - get "/products/all" - assert_response :success - assert assigns(:products) + module CustomDsl + def browses_site + get "/products/all" + assert_response :success + assert assigns(:products) + end end - end - def login(user) - open_session do |sess| - sess.extend(CustomDsl) - u = users(user) - sess.https! - sess.post "/login", username: u.username, password: u.password - assert_equal '/welcome', path - sess.https!(false) + def login(user) + open_session do |sess| + sess.extend(CustomDsl) + u = users(user) + sess.https! + sess.post "/login", username: u.username, password: u.password + assert_equal '/welcome', path + sess.https!(false) + end end - end end ``` Rake Tasks for Running your Tests --------------------------------- -You don't need to set up and run your tests by hand on a test-by-test basis. Rails comes with a number of rake tasks to help in testing. The table below lists all rake tasks that come along in the default Rakefile when you initiate a Rails project. - -| Tasks | Description | -| ------------------------------- | ----------- | -| `rake test` | Runs all unit, functional and integration tests. You can also simply run `rake` as the _test_ target is the default.| -| `rake test:benchmark` | Benchmark the performance tests| -| `rake test:controllers` | Runs all the controller tests from `test/controllers`| -| `rake test:functionals` | Runs all the functional tests from `test/controllers`, `test/mailers`, and `test/functional`| -| `rake test:helpers` | Runs all the helper tests from `test/helpers`| -| `rake test:integration` | Runs all the integration tests from `test/integration`| -| `rake test:mailers` | Runs all the mailer tests from `test/mailers`| -| `rake test:models` | Runs all the model tests from `test/models`| -| `rake test:profile` | Profile the performance tests| -| `rake test:recent` | Tests recent changes| -| `rake test:uncommitted` | Runs all the tests which are uncommitted. Supports Subversion and Git| -| `rake test:units` | Runs all the unit tests from `test/models`, `test/helpers`, and `test/unit`| - - -Brief Note About `Test::Unit` +You don't need to set up and run your tests by hand on a test-by-test basis. Rails comes with a number of commands to help in testing. The table below lists all commands that come along in the default Rakefile when you initiate a Rails project. + +| Tasks | Description | +| ----------------------- | ----------- | +| `rake test` | Runs all unit, functional and integration tests. You can also simply run `rake test` as Rails will run all the tests by default| +| `rake test:controllers` | Runs all the controller tests from `test/controllers`| +| `rake test:functionals` | Runs all the functional tests from `test/controllers`, `test/mailers`, and `test/functional`| +| `rake test:helpers` | Runs all the helper tests from `test/helpers`| +| `rake test:integration` | Runs all the integration tests from `test/integration`| +| `rake test:mailers` | Runs all the mailer tests from `test/mailers`| +| `rake test:models` | Runs all the model tests from `test/models`| +| `rake test:units` | Runs all the unit tests from `test/models`, `test/helpers`, and `test/unit`| + + +Brief Note About `MiniTest` ----------------------------- -Ruby ships with a boat load of libraries. One little gem of a library is `Test::Unit`, a framework for unit testing in Ruby. All the basic assertions discussed above are actually defined in `Test::Unit::Assertions`. The class `ActiveSupport::TestCase` which we have been using in our unit and functional tests extends `Test::Unit::TestCase`, allowing +Ruby ships with a boat load of libraries. Ruby 1.8 provides `Test::Unit`, a framework for unit testing in Ruby. All the basic assertions discussed above are actually defined in `Test::Unit::Assertions`. The class `ActiveSupport::TestCase` which we have been using in our unit and functional tests extends `Test::Unit::TestCase`, allowing us to use all of the basic assertions in our tests. +Ruby 1.9 introduced `MiniTest`, an updated version of `Test::Unit` which provides a backwards compatible API for `Test::Unit`. You could also use `MiniTest` in Ruby 1.8 by installing the `minitest` gem. + NOTE: For more information on `Test::Unit`, refer to [test/unit Documentation](http://ruby-doc.org/stdlib/libdoc/test/unit/rdoc/) +For more information on `MiniTest`, refer to [Minitest](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/minitest/unit/rdoc/) Setup and Teardown ------------------ @@ -862,10 +889,9 @@ class PostsControllerTest < ActionController::TestCase private - def initialize_post - @post = posts(:one) - end - + def initialize_post + @post = posts(:one) + end end ``` @@ -887,7 +913,7 @@ Testing mailer classes requires some specific tools to do a thorough job. ### Keeping the Postman in Check -Your mailer classes — like every other part of your Rails application — should be tested to ensure that it is working as expected. +Your mailer classes - like every other part of your Rails application - should be tested to ensure that it is working as expected. The goals of testing your mailer classes are to ensure that: @@ -919,19 +945,24 @@ require 'test_helper' class UserMailerTest < ActionMailer::TestCase tests UserMailer test "invite" do - @expected.from = 'me@example.com' - @expected.to = 'friend@example.com' - @expected.subject = "You have been invited by #{@expected.from}" - @expected.body = read_fixture('invite') - @expected.date = Time.now - - assert_equal @expected.encoded, UserMailer.create_invite('me@example.com', 'friend@example.com', @expected.date).encoded + # Send the email, then test that it got queued + email = UserMailer.create_invite('me@example.com', + 'friend@example.com', Time.now).deliver + assert !ActionMailer::Base.deliveries.empty? + + # Test the body of the sent email contains what we expect it to + assert_equal ['me@example.com'], email.from + assert_equal ['friend@example.com'], email.to + assert_equal 'You have been invited by me@example.com', email.subject + assert_equal read_fixture('invite').join, email.body.to_s end - end ``` -In this test, `@expected` is an instance of `TMail::Mail` that you can use in your tests. It is defined in `ActionMailer::TestCase`. The test above uses `@expected` to construct an email, which it then asserts with email created by the custom mailer. The `invite` fixture is the body of the email and is used as the sample content to assert against. The helper `read_fixture` is used to read in the content from this file. +In the test we send the email and store the returned object in the `email` +variable. We then ensure that it was sent (the first assert), then, in the +second batch of assertions, we ensure that the email does indeed contain what we +expect. The helper `read_fixture` is used to read in the content from this file. Here's the content of the `invite` fixture: @@ -943,9 +974,17 @@ You have been invited. Cheers! ``` -This is the right time to understand a little more about writing tests for your mailers. The line `ActionMailer::Base.delivery_method = :test` in `config/environments/test.rb` sets the delivery method to test mode so that email will not actually be delivered (useful to avoid spamming your users while testing) but instead it will be appended to an array (`ActionMailer::Base.deliveries`). +This is the right time to understand a little more about writing tests for your +mailers. The line `ActionMailer::Base.delivery_method = :test` in +`config/environments/test.rb` sets the delivery method to test mode so that +email will not actually be delivered (useful to avoid spamming your users while +testing) but instead it will be appended to an array +(`ActionMailer::Base.deliveries`). -However often in unit tests, mails will not actually be sent, simply constructed, as in the example above, where the precise content of the email is checked against what it should be. +NOTE: The `ActionMailer::Base.deliveries` array is only reset automatically in +`ActionMailer::TestCase` tests. If you want to have a clean slate outside Action +Mailer tests, you can reset it manually with: +`ActionMailer::Base.deliveries.clear` ### Functional Testing @@ -968,6 +1007,47 @@ class UserControllerTest < ActionController::TestCase end ``` +Testing helpers +--------------- + +In order to test helpers, all you need to do is check that the output of the +helper method matches what you'd expect. Tests related to the helpers are +located under the `test/helpers` directory. Rails provides a generator which +generates both the helper and the test file: + +```bash +$ rails generate helper User + create app/helpers/user_helper.rb + invoke test_unit + create test/helpers/user_helper_test.rb +``` + +The generated test file contains the following code: + +```ruby +require 'test_helper' + +class UserHelperTest < ActionView::TestCase +end +``` + +A helper is just a simple module where you can define methods which are +available into your views. To test the output of the helper's methods, you just +have to use a mixin like this: + +```ruby +class UserHelperTest < ActionView::TestCase + include UserHelper + + test "should return the user name" do + # ... + end +end +``` + +Moreover, since the test class extends from `ActionView::TestCase`, you have +access to Rails' helper methods such as `link_to` or `pluralize`. + Other Testing Approaches ------------------------ @@ -976,5 +1056,7 @@ The built-in `test/unit` based testing is not the only way to test Rails applica * [NullDB](http://avdi.org/projects/nulldb/), a way to speed up testing by avoiding database use. * [Factory Girl](https://github.com/thoughtbot/factory_girl/tree/master), a replacement for fixtures. * [Machinist](https://github.com/notahat/machinist/tree/master), another replacement for fixtures. +* [Fixture Builder](https://github.com/rdy/fixture_builder), a tool that compiles Ruby factories into fixtures before a test run. +* [MiniTest::Spec Rails](https://github.com/metaskills/minitest-spec-rails), use the MiniTest::Spec DSL within your rails tests. * [Shoulda](http://www.thoughtbot.com/projects/shoulda), an extension to `test/unit` with additional helpers, macros, and assertions. * [RSpec](http://relishapp.com/rspec), a behavior-driven development framework |