7.2. Example for a User in a Web Shop

Let's start with a user scaffold in an imaginary web shop:
$ rails new webshop
  [...]
$ cd webshop
$ rails generate scaffold user login_name first_name last_name birthday:date
      [...]
      invoke    test_unit
      create      test/unit/user_test.rb
      create      test/fixtures/users.yml
      [...]
      invoke    test_unit
      create      test/functional/users_controller_test.rb
      [...]
      invoke      test_unit
      create        test/unit/helpers/users_helper_test.rb
      [...]
$ rake db:migrate
      [...]
$
You already know all about scaffolds (if not, please go and read Chapter 5, Scaffolding and REST first) so you know what the application we have just created does. You have already seen that scaffold has created a few tests (they are easy to recognise because the word test is in the file name).
The complete test suite of a Rails project is processed with the command rake test. Let's have a go and see what a test produces at this stage of development:
$ rake test
Run options: 

# Running tests:



Finished tests in 0.003398s, 0.0000 tests/s, 0.0000 assertions/s.

0 tests, 0 assertions, 0 failures, 0 errors, 0 skips
Run options: 

# Running tests:

.......

Finished tests in 0.224044s, 31.2439 tests/s, 44.6341 assertions/s.

7 tests, 10 assertions, 0 failures, 0 errors, 0 skips
$ 
The output "7 tests, 10 assertions, 0 failures, 0 errors, 0 skips" looks good. By default, a test will run correctly in a standard scaffold.
Let's now edit the app/models/user.rb and insert a few validations (if these are not entirely clear to you, please read Section 4.15, “Validation”):
class User < ActiveRecord::Base
  attr_accessible :birthday, :first_name, :last_name, :login_name

  validates :login_name,
            :presence => true,
            :format => { 
                         :with => /^.*(?=.*[\-_.]).*$/,
                         :message => "must include at least one of the special characters -_."
                       }

  validates :last_name, 
            :presence => true

end
Then we execute rake test again:
$ rake test
Run options: 

# Running tests:



Finished tests in 0.003433s, 0.0000 tests/s, 0.0000 assertions/s.

0 tests, 0 assertions, 0 failures, 0 errors, 0 skips
Run options: 

# Running tests:

F.....F

Finished tests in 0.226569s, 30.8957 tests/s, 39.7230 assertions/s.

  1) Failure:
test_should_create_user(UsersControllerTest) [/Users/xyz/sandbox/webshop/test/functional/users_controller_test.rb:20]:
"User.count" didn't change by 1.
<3> expected but was
<2>.

  2) Failure:
test_should_update_user(UsersControllerTest) [/Users/xyz/sandbox/webshop/test/functional/users_controller_test.rb:39]:
Expected response to be a <:redirect>, but was <200>

7 tests, 9 assertions, 2 failures, 0 errors, 0 skips
Errors running test:functionals! #<RuntimeError: Command failed with status (2): [ruby -I"lib:test" -I"/Users/xyz/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.0.2/lib" "/Users/xyz/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.0.2/lib/rake/rake_test_loader.rb" "test/functional/**/*_test.rb" ]>
$
This time we have "2 failures". The error happens in the "should create user" and the "should update user". The explanation for this is in our validation. The example data created by the scaffold generator went through in the first rake test (without validation). The errors only occurred the second time (with validation).
This example data is created as fixtures in YAML format in the directory test/fixtures/. Let's have a look at the example data for User in the file test/fixtures/users.yml:
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html

one:
  login_name: MyString
  first_name: MyString
  last_name: MyString
  birthday: 2012-11-21

two:
  login_name: MyString
  first_name: MyString
  last_name: MyString
  birthday: 2012-11-21
There are two example records there that do not fulfil the requirements of our validation. The login_name should have at least one "-_." special character. Let's amend the login_name in test/fixtures/users.yml accordingly:
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html

one:
  login_name: My-String
  first_name: MyString
  last_name: MyString
  birthday: 2012-11-21

two:
  login_name: My-String
  first_name: MyString
  last_name: MyString
  birthday: 2012-11-21
Now, a rake test completes without any errors again:
$ rake test
Run options: 

# Running tests:



Finished tests in 0.003321s, 0.0000 tests/s, 0.0000 assertions/s.

0 tests, 0 assertions, 0 failures, 0 errors, 0 skips
Run options: 

# Running tests:

.......

Finished tests in 0.207182s, 33.7867 tests/s, 48.2667 assertions/s.

7 tests, 10 assertions, 0 failures, 0 errors, 0 skips
$
We now know that valid data has to be contained in the test/fixtures/users.yml so that the standard test created via scaffold will succeed. But nothing more. We now change the test/fixtures/users.yml to a minimum (for example, we do not need a first_name) and with data that is easier to read for humans:
one:
  login_name: horst.meier
  last_name: Meier

two:
  login_name: emil.stein
  last_name: Stein
To be on the safe side, let's do another rake test after making our changes (you really can't do that often enough):
$ rake test
Run options: 

# Running tests:



Finished tests in 0.003483s, 0.0000 tests/s, 0.0000 assertions/s.

0 tests, 0 assertions, 0 failures, 0 errors, 0 skips
Run options: 

# Running tests:

.......

Finished tests in 0.204765s, 34.1855 tests/s, 48.8365 assertions/s.

7 tests, 10 assertions, 0 failures, 0 errors, 0 skips
$ 

Important

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 have occurred:
  1) Failure:
test_should_create_user(UsersControllerTest) [/Users/xyz/sandbox/webshop/test/functional/users_controller_test.rb:20]:
"User.count" didn't change by 1.
<3> expected but was
<2>.

  2) Failure:
test_should_update_user(UsersControllerTest) [/Users/xyz/sandbox/webshop/test/functional/users_controller_test.rb:39]:
Expected response to be a <:redirect>, but was <200>

7 tests, 9 assertions, 2 failures, 0 errors, 0 skips
Errors running test:functionals! #<RuntimeError: Command failed with status (2): [ruby -I"lib:test" -I"/Users/xyz/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.0.2/lib" "/Users/xyz/.rvm/gems/ruby-1.9.3-p327/gems/rake-10.0.2/lib/rake/rake_test_loader.rb" "test/functional/**/*_test.rb" ]>
In the UsersControllerTest the User could not be created nor changed. The controller tests are located in the directory test/functional/. Let's now take a good look at the file test/functional/users_controller_test.rb:
require 'test_helper'

class UsersControllerTest < ActionController::TestCase
  setup do
    @user = users(:one)
  end

  test "should get index" do
    get :index
    assert_response :success
    assert_not_nil assigns(:users)
  end

  test "should get new" do
    get :new
    assert_response :success
  end

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

    assert_redirected_to user_path(assigns(:user))
  end

  test "should show user" do
    get :show, id: @user
    assert_response :success
  end

  test "should get edit" do
    get :edit, id: @user
    assert_response :success
  end

  test "should update user" do
    put :update, id: @user, user: { birthday: @user.birthday, first_name: @user.first_name, last_name: @user.last_name, login_name: @user.login_name }
    assert_redirected_to user_path(assigns(:user))
  end

  test "should destroy user" do
    assert_difference('User.count', -1) do
      delete :destroy, id: @user
    end

    assert_redirected_to users_path
  end
end
At the beginning, we 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 @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.

Important

For every test (in other words, at each run of rake test), a fresh and therefore empty test database is created automatically. This is a different database than the one that 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, accessing the index page:
  test "should get index" do
    get :index
    assert_response :success
    assert_not_nil assigns(:users)
  end
The command get :index accesses the page /users. assert_response :success means that the page was delivered. The line assert_not_nil assigns(:users) ensures that the controller does not pass the instance variable @users to the view with the value nil (setup ensures that there is already an entry in the database).[26]
Let's look more closely at the two problems from earlier. First, should create user:
  test "should create user" do
    assert_difference('User.count') do
      post :create, user: { birthday: @user.birthday, first_name: @user.first_name, last_name: @user.last_name, login_name: @user.login_name }
    end

    assert_redirected_to user_path(assigns(:user))
  end
The block assert_difference('User.count') do ... end expects a change by the code contained within it. User.count would have to result in 1 at the beginning and 2 at the end. But as we had an invalid set of data in the first test/fixtures/users.yml variation, the result of User.count was 0 at the beginning and at the end. 0 and not 1 at the beginning, because the setup do ... end was not able to work either.
The last line assert_redirected_to user_path(assigns(:user)) checks if after the newly created record the redirection to the corresponding view show occurs.
The second error occurred with should update user:
  test "should update user" do
    put :update, id: @user, user: { birthday: @user.birthday, first_name: @user.first_name, last_name: @user.last_name, login_name: @user.login_name }
    assert_redirected_to user_path(assigns(:user))
  end
Here, the record with the id of the @user record was supposed to be updated with the attributes of the @user record. Then, the show view for this record was again supposed to be displayed. Logically, this test could not work either, because a) the @user record did not exist in the database and b) it could not be updated as it was not valid.
Without commenting each individual functional test line by line, it is 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.

Tip

With rake test:functionals you can also run just the functional tests in the directory test/functional/.
$ rake test:functionals
Run options: 

# Running tests:

.......

Finished tests in 0.207113s, 33.7980 tests/s, 48.2828 assertions/s.

7 tests, 10 assertions, 0 failures, 0 errors, 0 skips
$ 

Unit Tests

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

Tip

With rake test, all tests present in the Rails project are executed. With rake test:units, only the unit tests in the directory test/unit/ are executed.
The unit tests are located in the directory test/unit/. But a look into the file test/unit/user_test.rb is rather sobering:
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end
By default, scaffold only writes a commented-out dummy test. That is why rake test:units runs through without any content:
$ rake test:units
Run options: 

# Running tests:



Finished tests in 0.003531s, 0.0000 tests/s, 0.0000 assertions/s.

0 tests, 0 assertions, 0 failures, 0 errors, 0 skips
$
A unit test always consists of the following structure:
test "an assertion" do
  assert something_is_true_or_false
end
The word assert already indicates that we 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 we have an error in the program (you can specify the output of the error as string at the end of the assert line).

Note

If you have a look around at http://guides.rubyonrails.org/testing.html then you will 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 the test/unit/user_test.rb:
require 'test_helper'

class UserTest < ActiveSupport::TestCase
  test 'an empty user is not valid' do
    assert !User.new.valid?, 'Saved an empty user.'
  end
end
This test checks if a newly created User that does not contain any data is valid. As assert only reacts to true, I placed a"!" before User.new.valid? to turn the false into a true, as an empty user cannot be valid.
So a rake test:units then completes immediately:
$ rake test:units
Run options: 

# Running tests:

.

Finished tests in 0.079557s, 12.5696 tests/s, 12.5696 assertions/s.

1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
$
Now we integrate two asserts in a test to check if the two fixture entries in the test/fixtures/users.yml are really valid:
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 a rake test:units:
$ rake test:units
Run options: 

# Running tests:

..

Finished tests in 0.085594s, 23.3661 tests/s, 35.0492 assertions/s.

2 tests, 3 assertions, 0 failures, 0 errors, 0 skips
$
Let's now insert tests for a few other login_name variations:
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

  [
    'hans.meier',
    'hans-meier',
    'h-meier',
    'h_meier',
    'h.meier2',
  ].each do |valid_login_name|
    test "the login_name '#{valid_login_name}' is valid" do
      assert User.new(last_name: users(:one).last_name, login_name: valid_login_name ).valid?, "login_name '#{valid_login_name}' is not valid."
    end
  end
end
Running through the test suite shows the results:
$ rake test:units
Run options: 

# Running tests:

.......

Finished tests in 0.088988s, 78.6623 tests/s, 89.8998 assertions/s.

7 tests, 8 assertions, 0 failures, 0 errors, 0 skips
$ 
With rake test you could now run all tests again.


[26] The symbol :users is used here to make sure that @users in the controller class to be tested is used, not @users in the test class itself.

Updates about this book will be published on my Twitter feed.