Test Driven Development

Notes for intro to TDD demonstration for Ncl Rug

Test Driven Development

Testing is often used as an afterthought. Even “Agile development” treats it as such, creating the sample project then adding in some tests afterwards. This is not TDD. TDD is a development method which goes roughly like this:

  • decide on an simple behaviour and write a test for it
  • run the test to make sure it fails
  • write just enough code for the test to pass
  • refactor
  • repeat

“Beginning Ruby on Rails eCommerce” Uses this approach.

Behaviour Driven Development

Started by Dan North as an aid to understanding TDD. Most important part is probably the acronym. TDD isn’t really about testing. The name BDD provides a more accurate image of the same process. There are some other useful ideas too. Some blindingly obvious, once you have seen them (always a good sign). Read Dan North’s blog for more details. Also check out RSpec, a “framework for practicing BDD in Ruby”
More BDD references that make interesting reading:

Things to mention

  • Goes against the grain. Why spend time writing tests?
  • Recommended unit is ridiculously small

Testing in Rails

3 types of testing, tailored to different areas:

  • Unit testing Tests models
  • Functional testing Tests a single controller
  • Integration testing Tests “user stories” probably involving multiple controllers

Unit testing

When a model is created by script/generate model ‘mymodel’ a test file (mymodel.rb) is created in test/unit. Any methods in this file that start with test_ will be run when the unit test is run. The generated file contains a single method that does very little except provide a useful template for the real tests.
validates statements can be used in a model to ensure that it’s fields conform to certain conditions. Ie
[ruby]
validates_presence_of :name
validates_uniqueness_of :number
[/ruby]
will cause MyModel.new to throw an exception which will typically be used by the create and edit methods to stop an incorrectly formed object from being saved. This behaviour can be tested by a method something like this:
[ruby]
def test_create
thing=Thing.new(
:name => ‘Iain’,
:surname => ‘Wood’
:number => 1
)
assert thing.save
end

def test_create_failing_no_name
thing=Thing.new
assert_equal false, thing.save
assert thing.errors.on(:name)
end
[/ruby]
Note: in test_create the :number => line is necessary or the test fails. This seems to suggest that validates_uniqueness_of does an implied validates_presence_of.

In simple cases there is very little to test in the models, hence the unit tests are quite simple. In some cases though it is useful to add more logic to the model, rather than relying on the straightforward access methods that are created directly from the database columns. We may want to have a simple method call that returns information based on several database fields, like a shoppingcart.total that is based on price and quantity of all the contents of the shopping cart.
Lets assume we have a use for a method thing.fullname that just returns the full name. First the test:
[code]
def test_fullname
thing = Thing.create(:name => ‘Iain’,
:surname => ‘Wood’,
:number => 3)
assert_equal ‘Iain Wood’, thing.fullname
end
[/code]
Naturaly this fails, because we haven’t written the code in models/thing.rb yet
[code]
def fullname
“#{name} #{surname}”
end
[/code]

Fixtures

To test that the validates_uniqueness statement in the model is working correctly we need to use a test fixture. The template for this is also created by the model generate script, and it is made accessible to the test file by the fixtures :mymodel statement that is automatically included. Objects defined in the fixtures file test/fixtures/mymodel.yml will be created at the beginning of each test. If we define one with :number set we can then define a test that tries to create another object with the same number. The validates_uniqueness statement should then fail. In the fixture file thing.yml:
[code]
thing_one:
id: 1
name: iain
wood: wood
number: 123
[/code]
and the unit test:
[ruby]
def test_create_failing_number_not_unique
thing=Thing.new(
:name => ‘Iain’,
:number => things(:thing_one).number
)
assert_equal false, thing.save
assert thing.errors.on(:number)
end
[/ruby]
things(:thing_one) is accessing the entry in the things fixture file called :thing_one (the only entry in this case)

Functional testing

Functional test files are created in test/functional when script/generate controller … is run. There is a bit more in the generated unit test files, but they are still basically empty tests to be extended into useful test cases. A typical test case may look like this:
[ruby]
def test_new
get :new
assert_template ‘thing/new’
assert_tag ‘h1’, :content => ‘Create new thing’
assert_tag ‘form’, :attributes => {:action => ‘/create’}
end
[/ruby]
This is testing the action, making sure that it uses the “new” template and that it has an H1 header containing “Create new thing” and a form element that points to the “create” action. This is the version for rails 1.1… Assert_tag will be depracated in rails 1.2 and replaced by assert_select. The above code should be replaced by something like this:
[ruby]
erm, how does assert_select work?
[/ruby]
We can write similar tests for each of our controllers methods. Obviously we need to take things a little further with certain methods. With “create” it would be a good idea to also check that the database gets updated with the new entry for instance. We could do that like this:
[ruby]
def test_create
get :new
assert_template ‘new’
assert_difference(Thing, :count) do
post :create, :thing => {:name => ‘Sam’,
:number => 7}
assert_response :redirect
assert_redirected_to :action => ‘index’
end
assert_equal ‘Thing Sam was successfully created.’, flash[:notice]
end
[/ruby]
This just checks that the number of things is increased by one. It uses a method assert_difference. This is placed in the file test/test_helper.rb and looks like this (lifted from Beginning eCommerce):
[ruby]
def assert_difference(object, method, difference=1)
initial_value = object.send(method)
yield
assert_equal initial_value + difference,
object.send(method)
end
def assert_no_difference(object, method, &block)
assert_difference object, method, 0, &block
end
[/ruby]

Integration testing

Integration testing tests the app as a whole. Typically each test would be based on a “User story” ie “Customer logs in, selects item for purchase and adds it to their shopping cart”.
The template file for an integration test needs to be specifically generated:
[code]
script/generate integration_test thing
[/code]
Integration testing uses a Domain Specific Language and the file generated, test/integration/thing_test.rb, will contain the template to create this for our test. The first part of the file contains the tests specified in the DSL, and the second contains the specifications of the DSL in a module. Here’s one lifted straight from the eCommerce book in case I don’t finish this in time…
[ruby]
require “#{File.dirname(__FILE__)}/../test_helper”
class DSLTest < ActionController::IntegrationTest def test_browse_book_store george = new_session bob = new_session george.add_book(...) bob.view_book(...) bob.add_book_to_cart(...) end private module TestingDSL def add_book(...) ... end def view_book(...) ... end def add_book_to_cart(...) ... end def new_session open_session do |sess| sess.extend(TestingDSL) yield sess if block_given? end end end end [/ruby]

Zentest

Zentest is designed to run tests for you. Monitors when code changes and runs the appropriate tests in the background. Could even be set up with growl so you don’t even need to check the results. zenspider blog has articles on how to go about using it, and Dave Astels blog has some info on how it can be used with BDD.

Running the demo

[code]
rails rug
cd rug
script/generate model thing
[/code]

  • Edit migrate file
  • Edit config/database.yml

[code]
rake db:migrate
rake db:test:clone_structure
[/code]

  • Edit thing.rb (add validates_presence_of and validates_uniqueness_of)
  • Edit test/unit/thing_test.rb (add test_create method)

You must be logged in to post a comment.