Wednesday, May 9, 2007

15 TDD steps to create a Rails application




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 Follow andrzejkrzywda on Twitter and subscribe to my RSS

37 comments:

Anonymous said...

Your code font is extremely hard to read... really detracts from what might be interesting reading. Perhaps it works on some platforms, certainly not on mine (unix with a reasonable selection of fonts)

Michael Foord said...

Fine from Windoze. :-)

Anonymous said...

I don't think its a platform specific issue so much as it is a readability issue caused by choice of font, size, colour and background colour.

IMO too many people's blogs are a mess of too-tiny fonts. Case in point, this blog. If you changed your CSS to resize pre.code to 120 or 130%, and most importantly, changed the background and foreground colour, your code display would be far more readable to all - not just those using Cleartext.

pre.code {
font-size: 120%;
border: solid 1px #aaa;
padding: 6px;
background-color: #eee;
color: inherit;
overflow:auto;
margin: 10px 0px;
}

The above or color: #333; is far better IMO.

FYI, unrelated to this issue, your CSS includes various references to a font named "Vedana"; I'm sure you mean "Verdana".

Not all readers have the eyes of a 20 something any more.

Speaking of readability, fuzzyman: waiting for google adverts to pop up before getting to your content does nothing for the readibility of your content. Sorry to be so grumbly but I frequently avoid reading your material because I can't stand waiting for the google ad to clear.

Perhaps the google advert revenue is worth it to you but if that's your primary motivation, why not write directly for ad sales then?

Michael Foord said...

Do you have a name 'Anonymous'.

As I said before - the combination of font size, family and colour (etc) is perfectly readable on Windows. So there must be *some* platform issue if you're finding it hard to read.

I don't think it is the adsense ads which are slow to load on my blog, I think it is a couple of the other javascript snippets (including amazon which I make very little revenue from - the google adverts I do quite well from).

There is a way to speed them up, simply loading the javascript *after* the content has loaded. I keep meaning to make the change - but haven't yet. Maybe I'll try and make time tonight.

Andrzej Krzywda said...

Anonymous,

Thanks for the css hint. I changed the pre.code style to the one you suggested and it looks fine. I hope it's now better for you.
All the 'Vedana' stuff is generated by blogspot. I didn't change the template except for adding the pre.code style. It's a pain to nicely display any code here.

Anonymous said...

andrzej,

Thanks for the CSS update ... although I didn't intend for you to take my suggestion completely literally, down to the colour choices. The real issue is some combos - green on black might have been popular when display terminals attached to mainframes (and early PC's) had nothing but that to offer, but its never been the most readable, especially for aging eyes.

fuzzyman, as for "its perfectly readable on windows", there is an implied "for me" at the end of that statement.

I'm sure you aren't suggesting that just because *you* can read it (I have windows machines in my test lab, rest assured) that everything must be ok.

There's nothing wrong with the font rendering on my platform; I'd not checked which font had been employed when I first made the comment; its clear that its fg/bg colour combo that is the factor here.

fg/bg colour combos do matter:

http://hubel.sfasu.edu/research/survreslts.html

Generally speaking, dark on light is more readable. Having said that, I code in vim in a light on dark screen, but in order to make it work for me, I had to do some extensive testing on fonts and sizing.

http://www.lighthouse.org/accessibility/effective-color-contrast/

Here's a useful aide: Check the bottom of this post for a helpful bookmarklet which I've just dragged to my own toolbar...

http://www.456bereastreet.com/archive/200608/light_text_on_dark_background_vs_readability/

Michael Foord said...

Sorry anonymous, I didn't read the bit about your eyes properly when I responded to your comment (although I wish I was still in my twenties)...

Mr Interested said...

Nicely done introduction to testing in Rails. I've been getting familiar with Rails recently but have skipped testing until now. This post served as a perfect "getting feet wet" tutorial.

Andrzej Krzywda said...

Mr Interested,

Thanks for a positive comment! I'm going to blog more about testing.

Antonio said...

Looks shiny, but your example uses assert_tag, whereas nowadays assert_select is typically a much better option (and I believe assert_tag is deprecated).

In this case, I believe the code would be:

assert_select 'div', /czesc/
assert_select 'div', /hello/

But I may be misunderstanding what the assert_tag code is doing (this code checks for a div with content that matches /czesc/, and a div with content that matches /hello/).

Unknown said...

Terrific tutorial.

To fix in Step 14: the rhtml is missing ampersands before "word".

matte said...

Thanks for your tutorial! I live very much TDD!

Aaron K. Hawkins said...

I like your post. Testing seems to be easiest when you start with it from day one of an application. I like how your tests drive your development. (Your first step was to call rake test - which told you your database wasn't configured.)

Posts like these will only help increase the practice of writing good tests. Thats great because we all have a vested interests in good tests - you never know when you'll be called upon to work on an application someone else has built.

I carpool with a good friend and we 've made some tshirts that promote good programming practices like testing. Check out this shirt design and let us know what you think.

Happy testing - and thanks for the article.

Anonymous said...

Can you elaborate on how using a code generator fits with the TDD approach of "write the test, let it fail, then write the code to make the test pass", and having that drive the API design and the choice of what application behavior is implemented?

For example, why is the application even trying to connect to a database? What test code has driven the need to add DB code?

Andrzej Krzywda said...
This comment has been removed by the author.
Andrzej Krzywda said...

Aaron,

Cool t-shirts!
It took ma a while to explain my friend (a non-IT person) why a piece of code can be "guilty" :-)

Andrzej Krzywda said...

matte, brian,

Thanks for nice comments!
Typo fixed.

Andrzej Krzywda said...

Anonymous,

I see your point here.

First, this article doesn't cover all aspects of testing an application. Normally, I would start with an acceptance test (acceptance meaning something like a Selenium test). The acceptance test would describe how the user interacts with the system. This is a good place to test (indirectly) that we use something that stores data (like a relational database).

The second part of creating the Words application covers the Word.add_content method which is more related to databases.

As for the code generation, except for the application skeleton (rake file, logs directories) I don't really generate code here. All the script/generator calls generate only stubs for classes (class and method declaration).

Does it answer your questions?

Anonymous said...

Great tutorial. This is sound advice for starting some good habits with rails. You're right about so many blogs emphasizing the quickness of rails and neglecting the testing.

Tomasz Nazar said...

Congrats Andrew!

Thank to this tutor this is my first hand on experience with Rails/Ruby/Rake/Gem/Sqlite/mocha - Oh My! :-)

Anonymous said...
This comment has been removed by a blog administrator.
Anonymous said...
This comment has been removed by a blog administrator.
Anonymous said...
This comment has been removed by a blog administrator.
Anonymous said...

Excellent website. Good work. Very useful. I will bookmark!

testando said...

Hello, Andrzej!
I find your post, as well as your blog, very interesting.
I'm starting to write a blog about rails and TDD as well.
I think your article it would be a great start for me.
My blog is at http://www.vip2web.com and is written in portuguese, so i'm here to ask you if i could translate this post and put into my blog?

Thanks in advance!

Best regards,

Vitor

Andrzej Krzywda said...

Hi Vitor,

Sure, you can translate it to portuguese! Send me a link when it's done :)

Andrzej

Anonymous said...
This comment has been removed by a blog administrator.
Unknown said...

On step 5, I'm getting this error:

[root@remandev migrate]# rake db:migrate
(in /usr/local/apache2/htdocs/testrails)
rake aborted!
uninitialized constant CreateWords

(See full trace by running task with --trace)

Just to verify, I'm supposed to replace the entire contents of the file db/migrate/001_create_words.rb with what is in the text field?

- Dave

Anonymous said...
This comment has been removed by a blog administrator.
Anonymous said...
This comment has been removed by a blog administrator.
Anonymous said...
This comment has been removed by a blog administrator.
Anonymous said...

Good web..........


rx pharmacy

Unknown said...

婚約指輪
結婚指輪
知多半島 ホテル
知多半島 温泉
知多半島 旅館
コンタクトレンズ
カラーコンタクト
カーボンオフセット
ゼネラリ
チューリッヒ
不動産
不動産投資
お見合いパーティー
浮気調査
賃貸
埼玉 不動産
群馬 不動産
海外推广
国际推广
网络营销
网络推广

Unknown said...

自動車保険 比較
自動車 保険 見積
出会い
出会い系
出会い系サイト
国際協力
治験
人権問題
盲導犬
自動車 保険 見積
三井ダイレクト
24そんぽ24
スニーカー
フランチャイズ
セルライト
タイ古式マッサージ
ソニー損保

Unknown said...

障害者
結婚相談所 横浜
結婚相談所 東京
広島 不動産
アクサダイレクト
野生動物
自動車保険
出会いサイト
アメリカンホームダイレクト
募金
不動産
アスクル

Unknown said...
This comment has been removed by a blog administrator.
Anonymous said...

物流网是现代物流产品设备资讯传媒. 中国水工业自动化网面向给排水领域设计院所、自来水厂、污水处理厂及市政管理部门,面向工业污水处理、-机器视觉
-传感器
-现场仪表
-显示控制仪表
-分析测试仪表
-执行机构
-工业安全
-低压电器
-电源
工业制水、水文水利、楼宇供水及水泵应用等水工业领域用户,发布和交流各种传感器、检测分析仪表、SCADA设备、监控系统及调速装置的产品、技术、应用、解决方案及市场信息;探讨、推进我国水工业自动化技术、节能技术应用发展。视频,多媒体,自动化,工控视频,自动化视频, PLC教程,变频器教程,软件教程,自动化行业视频新媒体的创造者和领先者-工控TV,教程,播客, PLC,可编程序控制器,自动化软件。同时产品频道有DCS -PAC- PC-BASED-CPCI- PXI-嵌入式系统-
SCADA

-自动化软件
-人机界面
-工业以太网
-现场总线
-无线通讯
-低压变频器
-高压变频器
-运动控制
-机械传动