Thursday, May 10, 2007

... and some more TDD steps with Rails

We were working together with Marcin on the Words application. It was a lot of fun. We were pair programming for about two hours. First, we started by going through the last TDD steps again. That was good as an exercise and also it was a good introduction for Marcin (he was driving the keyboard during this part) to Mac, TextMate, Rake and Autotest. After we repeated all the steps, we decided that we want to have a way of feeding our database with more words. The idea was to have a form with a text box where a user could paste any kind of text. The text would then be parsed into words which are put into the database.
It was obvious that we need some kind of Word.add_content method. So we started with tests for that method. The final result of those tests is as following:

def assert_count_after_add(count, content)
Word.add_content(content)
assert_equal count, Word.count
end

def test_add_content
assert_count_after_add 2, ""
assert_count_after_add 3, "single"
assert_count_after_add 5, "two words"
assert_count_after_add 9, "four wordzz are funny"
assert_count_after_add 10, "duplicate duplicate"
end

As you can see there is a custom assertion. We also test that if there is already a word in the database we don't want it to be added again.
The implementation for the add_content:

def self.add_content content
if not content.empty?
content.split(" ").each {|eng_word|
if Word.find(:first,
:conditions => "eng = '#{eng_word}'") == nil
Word.new(:eng=>eng_word).save
end
}
end
end

It's not the prettiest piece of code but it should be fine for now.
Oops, it looks like we don't have a test for the fact that words are saved with their english translation only...
After implementing the add_content method we realized that now our database could be filled with words that are not translated (pl field is empty). It means we want to change the implementation of the 'Word.random' method so that it only displays words that are already translated. There is no point in displaying the english version only...
It sounds like we need word.translated? method. Let's write some tests:

def test_translated
assert !Word.new.translated?
assert (Word.new :pl=>"tak", :eng=>"yes").translated?
assert !(Word.new :pl=>"tak").translated?
assert !(Word.new :eng=>"yes").translated?
end

The implementation is simple:

def translated?
pl and eng
end

Now, it's time for adding a test for the fact that Word.random only returns translated words. Again, we call the test several times so that we can assume it works. We can probably refactor it later to some nicer way. We add a word to the database and then assert that it wasn't chosen even after 100 calls. Any ideas how to test it better?

def test_use_words_with_translation
Word.new( :pl=>'tak', :eng=>'yes').save
Word.add_content("hello")
randoms = []
100.times {randoms << Word.random.eng}
assert (randoms.include? "hello") == false
end

The new implementation looks like that:

def self.random
(Word.find(:all).select {|word|word.translated?}.sort_by {rand})[0]
end

Unfortunately, there was no time to create a user interface for adding a content. Sounds, like a nice topic for the next session.

3 comments:

Anonymous said...

nice second part - hope that there will be a third (and more).

just two things..

with the fixture from part one still in place, the new test_add_content didn't work. I had to add a +2 to get it green.

in the first part you used "eng" as database column and in the second part "en" - would be nice if both (and future parts) would use the same names (I changed all to "eng").

so long,
Hendrik

Andrzej Krzywda said...

Hi hendrik,

Well spotted! Thanks!
Fixed both problems.

Using fixtures causes problems like that. I'm moving more into the direction of mock objects now. Take a look my new article from the "Words application series":

http://andrzejonsoftware.blogspot.com/2007/06/testing-rails-controllers-with-mock.html

John G. Ferguson said...

Thanks! Overall very helpful and I had to learn a lot about the right way to use Rails migrations.

BTW, the first part of the tutorial about the tests should fail... does not seem to hold true with Rails 2 on my Mac. They run without issuing any message. Then once I add a model object... when I rake the tests I get told to run a migration first.

That's when my migrations issues started.

However, it is nice to work through all this in TDD with that goal. I look forward to the next post.