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 .
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:
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:
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.
app/models/user.rb
Then execute rails test again, as shown here:
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.
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.
test/fixtures/users.yml
Now, the rails test command completes without any errors again.
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.
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).
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:
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.
test/controllers/users_controller_test.rb
At the beginning, you will find a setup instruction.
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.
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.
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.
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.
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:
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).
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.
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.
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.
Then once more there’s a rails test command .
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).
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.
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.
You can now populate the file test/integration/invalid_new_user_workflow_test.rb with the test shown in Listing 9-10.
test/integration/invalid_new_user_workflow_test.rb
Let’s run all the tests .
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:
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.