© Stefan Wintermeyer 2018
Stefan WintermeyerLearn Rails 5.2https://doi.org/10.1007/978-1-4842-3489-1_9

9. Tests

Stefan Wintermeyer1 
(1)
Bochum, Germany
 

I have been programming for more than 30 years, and most of the time I have managed quite well without test-driven development (TDD). I am not going to be mad at you if you decide to just skip this chapter. You can create Rails applications without tests, and you are not likely to garner any bad karma as a result (at least, I hope not, but you can never be entirely sure with the whole karma thing).

If you should decide to go for TDD, then I can promise you that it is enlightening. The basic idea of TDD is that you write a test for each programming function to check that function. In the pure TDD teaching, this test is written before the actual programming. Yes, you will have a lot more to do initially. But later, you can run all the tests and see that the application works exactly as you wanted it to work. The real advantage becomes apparent only after a few weeks or months when you look at the project again and write an extension or new variation. Then you can safely change the code and check that it still works properly by running the tests. This avoids a situation where you find yourself saying “Oops, that went a bit wrong; I just didn’t think of that particular problem.”

Often, the advantage of TDD is evident when writing a program. Tests can reveal many careless mistakes that you would otherwise have stumbled across only much later.

This chapter is a brief overview of the topic of test-driven development with Rails. If you want to find out more, you can dive into the official Rails documentation at http://guides.rubyonrails.org/testing.html .

../images/460214_1_En_9_Chapter/460214_1_En_9_Figa_HTML.gif TDD is just like driving a car. The only way to learn it is by doing it.

Example for a User in a Web Shop

Let’s start with a user scaffold in an imaginary web shop , as shown here:

$ rails new webshop
  [...]
$ cd webshop
$ rails generate scaffold user login_name first_name last_name birthday:date
    [...]
    invoke    test_unit
    create      test/models/user_test.rb
    create      test/fixtures/users.yml
    [...]
    invoke    test_unit
    create      test/controllers/users_controller_test.rb
    create      test/system/users_test.rb
    invoke    helper
    create      app/helpers/users_helper.rb
    invoke      test_unit
    [...]
$ rails db:migrate
    [...]

You already know all about scaffolds (if not, please read Chapter 4), so you know what the application you have just created does. The scaffold created a few tests (they are easy to recognize because the word test is in the file names).

The complete test suite of a Rails project is processed with the command rails test. Let’s see what a test produces at this stage of development:

$ rails test
Running via Spring preloader in process 2440
Run options: --seed 62885
# Running:
.......
Finished in 1.361143s, 5.1427 runs/s, 6.6121 assertions/s.
7 runs, 9 assertions, 0 failures, 0 errors, 0 skips

The output 7 runs, 9 assertions, 0 failures, 0 errors, 0 skips looks good. By default, a test will run through in a standard scaffold.

Let’s edit app/models/user.rb and insert a few validations (if these are not entirely clear to you, please read the section “Validation” in Chapter X), as shown in Listing 9-1.

class User < ApplicationRecord
  validates :login_name,
            presence: true,
            length: { minimum: 10 }
  validates :last_name,
            presence: true
end
Listing 9-1

app/models/user.rb

Then execute rails test again, as shown here:

$ rails test
Running via Spring preloader in process 89164
Run options: --seed 40163
# Running:
....F
Failure:
UsersControllerTest#test_should_update_user [/.../webshop/test/controllers/users_controller_test.rb:38]:
Expected response to be a <3XX: redirect>, but was a <200: OK>
bin/rails test test/controllers/users_controller_test.rb:36
F
Failure:
UsersControllerTest#test_should_create_user [/.../webshop/test/controllers/users_controller_test.rb:19]:
"User.count" didn't change by 1.
Expected: 3
  Actual: 2
bin/rails test test/controllers/users_controller_test.rb:18
.
Finished in 0.262099s, 26.7075 runs/s, 30.5228 assertions/s.
7 runs, 8 assertions, 2 failures, 0 errors, 0 skips

Boom! This time you have the output 2 failures. The error happens in UsersControllerTest#test_should_update_user and UsersControllerTest#test_should_create_user. The explanation for this is in the validation. The example data created by the scaffold generator went through in the first rails test test (without validation). The errors occurred only the second time (with validation).

This example data is created as fixtures_tests tests in YAML format in the directory test/fixtures/. Let’s take a look at the example data for User in the file test/fixtures/users.yml; see Listing 9-2.

one:
  login_name: MyString
  first_name: MyString
  last_name: MyString
  birthday: 2018-01-25
two:
  login_name: MyString
  first_name: MyString
  last_name: MyString
  birthday: 2018-01-25
Listing 9-2

test/fixtures/users.yml

There are two example records in Listing 9-2 that do not fulfill the requirements of the validation. The login_ name record should have a length of at least ten. Let’s change the login_name record in test/fixtures/users.yml accordingly; see Listing 9-3.

one:
  login_name: MyString12
  first_name: MyString
  last_name: MyString
  birthday: 2018-01-25
two:
  login_name: MyString12
  first_name: MyString
  last_name: MyString
  birthday: 2018-01-25
Listing 9-3

test/fixtures/users.yml

Now, the rails test command completes without any errors again.

$ rails test
Running via Spring preloader in process 89807
Run options: --seed 50152
# Running:
.......
Finished in 0.271182s, 25.8129 runs/s, 33.1880 assertions/s.
7 runs, 9 assertions, 0 failures, 0 errors, 0 skips

Now you know that valid data has to be contained in test/fixtures/users.yml so that the standard test created via the scaffolding will succeed. But you need nothing more. The next step is to change test/fixtures/users. yml to the minimum needed (for example, you do not need a first_name field), as shown in Listing 9-4.

one:
  login_name: MyString12
  last_name: Mulder
two:
  login_name: MyString12
  last_name: Scully
Listing 9-4

test/fixtures/users.yml

To be on the safe side, let’s run another rails test command after making the changes (you really can’t do that often enough).

$ rails test
Running via Spring preloader in process 89972
Run options: --seed 40198
# Running:
.......
Finished in 0.255256s, 27.4234 runs/s, 35.2587 assertions/s.
7 runs, 9 assertions, 0 failures, 0 errors, 0 skips

../images/460214_1_En_9_Chapter/460214_1_En_9_Figb_HTML.gif All fixtures are loaded into the database when a test is started. You need to keep this in mind for your test, especially if you use uniqueness in your validation.

Functional Tests

Let’s take a closer look at the point where the original errors occurred, as shown here:

Failure:
UsersControllerTest#test_should_create_user
[/.../webshop/test/controllers/users_controller_test.rb:19]:
"User.count" didn't change by 1.
Expected: 3
  Actual: 2

In UsersControllerTest , the user could not be created. The controller tests are located in the directory test/functional/. Let’s now take a good look at the file test/controllers/users_controller_test.rb, as shown in Listing 9-5.

require 'test_helper'
class UsersControllerTest < ActionDispatch::IntegrationTest
  setup do
    @user = users(:one)
  end
  test "should get index" do
    get users_url
    assert_response :success
  end
  test "should get new" do
    get new_user_url
    assert_response :success
  end
  test "should create user" do
    assert_difference('User.count') do
      post users_url, params: { user: { birthday: @user.birthday,
      first_name: @user.first_name, last_name: @user.last_name,
      login_name: @user.login_name } }
    end
    assert_redirected_to user_url(User.last)
  end
  test "should show user" do
    get user_url(@user)
    assert_response :success
  end
  test "should get edit" do
    get edit_user_url(@user)
    assert_response :success
  end
  test "should update user" do
    patch user_url(@user), params: { user: { birthday: @user.birthday,
    first_name: @user.first_name, last_name: @user.last_name,
    login_name: @user.login_name } }
    assert_redirected_to user_url(@user)
  end
  test "should destroy user" do
    assert_difference('User.count', -1) do
      delete user_url(@user)
    end
    assert_redirected_to users_url
  end
end
Listing 9-5

test/controllers/users_controller_test.rb

At the beginning, you will find a setup instruction.

setup do
  @user = users(:one)
end

These three lines of code mean that for the start of each individual test, an instance called @user with the data of the item one from the file test/fixtures/users.yml is created. setup is a predefined callback that—if present—is started by Rails before each test. The opposite of setup is teardown. A teardown—if present—is called automatically after each test.

../images/460214_1_En_9_Chapter/460214_1_En_9_Figc_HTML.gif For every test (in other words, at each run of rails test), a fresh and therefore empty test database is created automatically. This is a different database than the one you access by default via rails console (that is, the development database). The databases are defined in the configuration file config/database.yml. If you want to do debugging, you can access the test database with rails console test.

This functional test then tests various web page functions. First, you access the index page.

test "should get index" do
  get users_url
  assert_response :success
end

The command get users_ url accesses the page /users. A response of assert_response :success means that the page was delivered.

Let’s look more closely at the should create user problem from earlier.

test "should create user" do
  assert_difference('User.count') do
    post users_url, params: { user: { birthday: @user.birthday,
    first_name: @user.first_name, last_name: @user.last_name,
    login_name: @user.login_name } }
  end
  assert_redirected_to user_url(User.last)
end

The block assert_difference('User.count') do ... end expects a change by the code contained within it. User.count should result in +1.

The last line, assert_redirected_to user_path(User.last), checks whether after the newly created record the redirection to the corresponding view show occurs.

Without describing each individual functional test line by line, it’s becoming clear what these tests do: they execute real queries to the web interface (or actually to the controllers), and so they can be used for testing the controllers.

Unit Tests

For testing the validations that you have entered in app/models/user.rb, unit tests are more suitable. Unlike the functional tests, these test only the model, not the controller’s work.

The unit tests are located in the directory test/models/. But a look into the file test/models/user_test.rb is rather sobering, as shown in Listing 9-6.

require 'test_helper'
class UserTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end
Listing 9-6

test/models/user_test.rb

By default, the scaffold only writes a commented-out dummy test.

A unit test always consists of the following structure:

test "an assertion" do
  assert something_is_true
end

The word assert already indicates that you are dealing with an assertion in this context. If this assertion is true, the test will complete, and all is well. If this assertion is false, the test fails, and you have an error in the program (you can specify the output of the error as a string at the end of the assert line).

If you take a look at http://guides.rubyonrails.org/testing.html , you’ll see that there are some other assert variations. Here are a few examples:
  • assert( boolean, [msg] )

  • assert_equal( obj1, obj2, [msg] )

  • assert_not_equal( obj1, obj2, [msg] )

  • assert_same( obj1, obj2, [msg] )

  • assert_not_same( obj1, obj2, [msg] )

  • assert_nil( obj, [msg] )

  • assert_not_nil( obj, [msg] )

  • assert_match( regexp, string, [msg] )

  • assert_no_match( regexp, string, [msg] )

Let’s breathe some life into the first test in test/unit/user_test.rb, as shown in Listing 9-7.

require 'test_helper'
class UserTest < ActiveSupport::TestCase
  test 'a user with no attributes is not valid' do
    user = User.new
    assert_not user.save, 'Saved a user with no attributes.'
  end
end
Listing 9-7

test/unit/user_test.rb

This test checks whether a newly created User that does not contain any data is valid (it shouldn’t be).

You can run a rails test command for the complete test suite.

$ rails test
Running via Spring preloader in process 91049
Run options: --seed 8014
# Running:
........
Finished in 0.248883s, 32.1436 runs/s, 40.1795 assertions/s.
8 runs, 10 assertions, 0 failures, 0 errors, 0 skips

Now you integrate two asserts in a test to check whether the two fixture entries in test/fixtures/users.yml are really valid. The first one is just a shorter version of the empty user test.

require 'test_helper'
class UserTest < ActiveSupport::TestCase
  test 'an empty user is not valid' do
    assert !User.new.valid?, 'Saved an empty user.'
  end
  test "the two fixture users are valid" do
    assert User.new(last_name: users(:one).last_name, login_name:
    users(:one).login_name ).valid?, 'First fixture is not valid.'
    assert User.new(last_name: users(:two).last_name, login_name:
    users(:two).login_name ).valid?, 'Second fixture is not valid.'
  end
end

Then once more there’s a rails test command .

$ rails test
Running via Spring preloader in process 91434
Run options: --seed 57493
# Running:
.........
Finished in 0.256179s, 35.1317 runs/s, 46.8422 assertions/s.
9 runs, 12 assertions, 0 failures, 0 errors, 0 skips

Fixtures

With fixtures you can generate example data for tests. The default format for this is YAML. You can find the files for this in the directory test/fixtures/; they are automatically created with rails generate scaffold. But of course you can also define your own files. All fixtures are loaded into the test database by default with every test.

You can find examples for alternative formats (e.g., CSV) at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html .

Static Fixtures

The simplest variant for fixtures is static data. The fixture for User used in the section “Example for a User in a Web Shop” statically should look like Listing 9-8 (please change the content of the file accordingly).

one:
  login_name: fox.mulder
  last_name: Mulder
two:
  login_name: dana.scully
  last_name: Scully
Listing 9-8

test/fixtures/users.yml

You simply write the data in YAML format into the corresponding file.

Fixtures with erb

Static YAML fixtures are sometimes not smart enough to do the job. In these cases, you can work with erb.

If you want to dynamically enter today’s date 20 years ago for a birthday, then you can simply do it with erb in test/fixtures/users.yml, as shown in Listing 9-9.

one:
  login_name: fox.mulder
  last_name: Mulder
  birthday: <%= 20.years.ago.to_s(:db) %>
two:
  login_name: dana.scully
  last_name: Scully
  birthday: <%= 20.years.ago.to_s(:db) %>
Listing 9-9

test/fixtures/users.yml

Integration Tests

Integration tests are tests that work like functional tests, but they can span several controllers and additionally analyze the content of a generated view. So, you can use them to re-create complex workflows within a Rails application. As an example, I will show how to write an integration test that tries to create a new user via the web GUI but omits the login_name value and consequently gets corresponding flash error messages.

A rails generate scaffold command generates unit and functional tests but not integration tests. You can do this either manually in the directory test/integration/ or more comfortably with rails generate integration_test. So, let’s create an integration test.

$ rails generate integration_test invalid_new_user_workflow
Running via Spring preloader in process 91538
      invoke  test_unit
      create    test/integration/invalid_new_user_workflow_test.rb

You can now populate the file test/integration/invalid_new_user_workflow_test.rb with the test shown in Listing 9-10.

require 'test_helper'
class InvalidNewUserWorkflowTest < ActionDispatch::IntegrationTest
  fixtures :all
  test 'try to create a new user without a login' do
    @user = users(:one)
    get '/users/new'
    assert_response :success
    post users_url, params: { user: { last_name: @user.last_name } }
    assert_equal '/users', path
    assert_select 'li', "Login name can't be blank"
    assert_select 'li', "Login name is too short (minimum is 10 characters)"
  end
end
Listing 9-10

test/integration/invalid_new_user_workflow_test.rb

Let’s run all the tests .

$ rails test
Running via Spring preloader in process 91837
Run options: --seed 4153
# Running:
..........
Finished in 0.277714s, 36.0083 runs/s, 57.6132 assertions/s.
10 runs, 16 assertions, 0 failures, 0 errors, 0 skips

The example clearly shows that you can program without manually using a web browser to try it. Once you have written a test for the corresponding workflow, you can rely in the future on the fact that it will run through; in other words, you don’t have to try it manually in the browser as well.

rails stats

With rails stats, you can get an overview of your Rails project. Here’s an example:

$ rails stats
+----------------------+--------+--------+---------+---------+-----+-------+
| Name                 |  Lines |    LOC | Classes | Methods | M/C | LOC/M |
+----------------------+--------+--------+---------+---------+-----+-------+
| Controllers          |     77 |     53 |       2 |       9 |   4 |     3 |
| Helpers              |      4 |      4 |       0 |       0 |   0 |     0 |
| Jobs                 |      2 |      2 |       1 |       0 |   0 |     0 |
| Models               |     11 |     10 |       2 |       0 |   0 |     0 |
| Mailers              |      4 |      4 |       1 |       0 |   0 |     0 |
| Channels             |      8 |      8 |       2 |       0 |   0 |     0 |
| JavaScripts          |     31 |      4 |       0 |       1 |   0 |     2 |
| Libraries            |      0 |      0 |       0 |       0 |   0 |     0 |
| Controller tests     |     48 |     38 |       1 |       7 |   7 |     3 |
| Helper tests         |      0 |      0 |       0 |       0 |   0 |     0 |
| Model tests          |     14 |     12 |       1 |       2 |   2 |     4 |
| Mailer tests         |      0 |      0 |       0 |       0 |   0 |     0 |
| Integration tests    |     17 |     13 |       1 |       1 |   1 |    11 |
| System tests         |      9 |      3 |       1 |       0 |   0 |     0 |
+----------------------+--------+--------+---------+---------+-----+-------+
| Total                |    225 |    151 |      12 |      20 |   1 |     5 |
+----------------------+--------+--------+---------+---------+-----+-------+
  Code LOC: 88     Test LOC: 63     Code to Test Ratio: 1:0.7

In this project, there are a total of 88 lines of code (LOCs) in the controllers, helpers, and models. There are a total of 63 LOCs for tests. This gives you a test relation of 1:1.0.7. Logically, this does not say anything about the quality of tests.

More on Testing

This chapter just scratched the surface of the topic of TDD in Rails. Take a look at http://guides.rubyonrails.org/testing.html for more information. There you will also find several good examples on this topic.

One cool feature of Ruby on Rails testing is the ability to run the tests in real browsers (e.g., Chrome) and to take screenshots while doing so.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.166.90