Hi,
Testing Rails applications is my passion for over 7 years now. If you sign up to this newsletter you will receive exclusive information about everything related to Ruby unit testing, Rails acceptance testing, JavaScript testing, testable architectures, TDD, BDD and good OO design.
Subscribe to the Testing Rails mailing list
Introduction
Several times recently, I have been asked how to develop a Rails application using the Test Driven Development approach. I'm not an expert here, but I've put together some notes on how to start working on a Rails application whilst being test-driven all the time.
As an example I will use a word-learning web application. The simplest use case is to display a random word object (with its Polish translation) from the database.
Every time we refresh we want to see a different word.
1. Create a new Rails application
rails my_app
cd my_app
Run tests with 'rake test'. It fails due to missing database configuration.
2. Set up the databases - config/database.yml
The code below assumes sqlite3 databases.
development:
adapter: sqlite3
database: db/my_app_development.sqlite
test:
adapter: sqlite3
database: db/my_app_test.sqlite
'rake test' now runs fine.
3. Create a Word class with a corresponding unit test
script/generate model Word
4. Write a unit test for the Word class. Edit the test/unit/word_test.rb.
def test_word_is_english_and_polish
word = Word.new :eng=>'never', :pl=>'nigdy'
assert_equal 'never', word.eng
assert_equal 'nigdy', word.pl
end
'rake test' now fails due to missing words table.
5. Edit db/migrate/001_create_words.rb
We are using a migration here in order to create a table. It's a recommended way of dealing with database changes.
def self.up
create_table :words do |t|
t.column :eng, :string
t.column :pl, :string
end
Word.new(:eng=>'yes', :pl=>'tak').save
Word.new(:eng=>'no', :pl=>'nie').save
Word.new(:eng=>'everything', :pl=>'wszystko').save
end
def self.down
drop_table :words
end
The sample words that we are adding use Word.new .. lines, will be added to the development database. It's important to distinguish the 'test' and 'development' database. The first one is only used during tests. The latter is used by default when you start the application.
Apply the migration with 'rake db:migrate'.
'rake test' now succeeds with the following:
'1 tests, 2 assertions, 0 failures, 0 errors'
6. Fixtures and test for word.random. Edit word_test again.
It's not easy to test a method which behaves randomly. Let's assume that it's enough to test that if we have only two words in our database then one of them should be called at least once per 10 calls.
fixtures :words
def test_random
results = []
10.times {results << Word.random.eng}
assert results.include?("yes")
end
Note the 'fixtures :words' line. Edit the 'words.yml' file.
yes:
id: 1
pl: 'tak'
eng: 'yes'
no:
id: 2
pl: 'nie'
eng: 'no'
This will be loaded to the test database before every run of tests.
7. Implement the Word.random method
def self.random
all = Word.find :all
all[rand(all.size)]
end
Warning: The code above could be slow for many words in a database (we retrieve all words only for selecting a random element). It's good enough for our needs.
8. Generate the Words controller with a 'learn' action
script/generate controller Words learn
9. Write a test for the learn method Just as there is a one-to-one ratio between unit tests and models, so there is between functional tests and controllers. The Controller's responsibility is to retrieve objects from the Model layer and pass them to the View. Let's test the View part first. We use the 'assigns' collection which contains all the objects passed to the View.
def test_learn_passes_a_random_word
get 'learn'
assert_kind_of Word, assigns('word')
end
10. Make the Test Pass
def learn
@word = Word.new
end
11. Write more tests in the words_controller_test How can we test that controller uses the Word.random method? We don't want to duplicate the tests for the Word.random method. Mocks to the rescue! We will only test that the controller calls the Word.random method. The returned value will be faked with a prepared word. Let's install the
mocha framework:
gem install mocha
Now we can use 'expects' and 'returns' methods. 'expects' is used for setting an expectation on an object or a class. In this case we expect that the 'random' method will be called. We also set a return value by using 'returns' method. Setting a return value means faking (stubbing) the real method. The real Word.random won't be called. If an expectation isn't met the test fails.
require 'mocha'
def test_learn_passes_a_random_word
random_word = Word.new
Word.expects(:random).returns(random_word)
get 'learn'
assert_equal random_word, assigns('word')
end
'rake test' now fails. The Word.method wasn't called.
12. Rewrite the implementation
def learn
@word = Word.random
end
'rake test' now passes.
13. Test that a word is displayed: Extend the existing test with assert_tag calls.
def test_learn_passes_a_random_word
random_word = Word.new(:pl=>'czesc', :eng=>'hello')
Word.expects(:random).returns(random_word)
get 'learn'
assert_equal random_word, assigns('word')
assert_tag :tag=>'div', :child => /czesc/
assert_tag :tag=>'div', :child => /hello/
end
14. Implement the view - learn.rhtml
<div>
<%= @word.eng %>
<%= @word.pl %>
</div>
15. Manual testing
script/server
Go to 'http://localhost:3000/words/learn'. Refresh several times.
Related articles ... and some more TDD steps with Rails Testing Rails controllers with mock objects If you want to read more about testing in Rails go to the
Guide To Testing The Rails.
If you read this far you should
and
subscribe to my RSS