Writing Unit Tests
Credit: Steve Arneil
Problem
You want to write some unit tests for your software, to guarantee its correctness now and in the future.
Solution
Use Test::Unit, the Ruby unit testing framework, from the Ruby standard library.
Consider a simple class for storing the name of a person. The Person class shown below stores a first name, a last name, and an age: a persons full name is available as a computed value. This code might go into a Ruby script called app/person.rb:
# app/person.rb class Person attr_accessor :first_name, :last_name, :age def initialize(first_name, last_name, age) raise ArgumentError, "Invalid age: #{age}" unless age > 0 @first_name, @last_name, @age = first_name, last_name, age end def full_name first_name + + last_name end end
Now, lets write some unit tests for this class. By convention, these would go into the file test/person_test.rb.
First, require the Person class itself and the Test::Unit framework:
# test/person_test.rb require File.join(File.dirname(__FILE__), .., app, person) require est/unit
Next, extend the framework class Test::Unit::TestCase with a class to contain the actual tests. Each test should be written as a method of the test class, and each test method should begin with the prefix test. Each test should make one or more assertions: statements about the code which must be true for the code to be correct. Below are three test methods, each making one assertion:
class PersonTest < Test::Unit::TestCase def test_first_name person = Person.new(Nathaniel, Talbott, 25) assert_equal Nathaniel, person.first_name end def test_last_name person = Person.new(Nathaniel, Talbott, 25) assert_equal Talbott, person.last_name end def test_full_name person = Person.new(Nathaniel, Talbott, 25) assert_equal Nathaniel Talbott, person.full_name end def test_age person = Person.new(Nathaniel, Talbott, 25) assert_equal 25, person.age assert_raise(ArgumentError) { Person.new(Nathaniel, Talbott, -4) } assert_raise(ArgumentError) { Person.new(Nathaniel, Talbott, four) } end end
This code is somewhat redundant; see below for a way to fix that issue. For now, lets run our four tests, by running person_test.rb as a script:
$ ruby test/person_test.rb Loaded suite test/person_test Started …. Finished in 0.008837 seconds. 4 tests, 6 assertions, 0 failures, 0 errors
Great! All the tests passed.
Discussion
The PersonTest class defined above works, but its got some redundant and inefficient code. Each of the four tests starts by creating a Person object, but they could all share the same Person object. The test_age method needs to create some additional, invalid Person objects to verify the error checking, but theres no reason why it can share the same "normal" Person object as the other three test methods.
Test::Unit makes it possible to refactor shareable code into a method named setup. If a test class has a setup method, it will be called before any of the assertion methods. Conversely, any clean-up code that is required after each test method runs can be placed in a method named teardown.
Heres a new implementation of PersonTest that uses setup and class constants to remove the duplicate code:
# person2.rb require File.join(File.dirname(__FILE__), .., app, person) require est/unit class PersonTest < Test::Unit::TestCase FIRST_NAME, LAST_NAME, AGE = Nathaniel, Talbott, 25 def setup @person = Person.new(FIRST_NAME, LAST_NAME, AGE) end def test_first_name assert_equal FIRST_NAME, @person.first_name end def test_last_name assert_equal LAST_NAME, @person.last_name end def test_full_name assert_equal FIRST_NAME + + LAST_NAME, @person.full_name end def test_age assert_equal 25, @person.age assert_raise(ArgumentError) { Person.new(FIRST_NAME, LAST_NAME, -4) } assert_raise(ArgumentError) { Person.new(FIRST_NAME, LAST_NAME, four) } end end
There are lots of assertion methods besides the assert_equal and assert_raise method used in the test classes above: assert_not_equal, assert_nil, and more exotic methods like assert_respond_to. All the assertion methods are defined in the Test::Unit::Assertions module, which is mixed into the Test::Unit::TestCase class.
The simplest assertion method is just plain assert. It causes the test method to fail unless its passed a value other than false or nil:
def test_first_name assert(FIRST_NAME == @person.first_name) end
assert is the most basic assertion method. All the other assertion methods can be defined in terms of it:
def assert_equal(expected, actual) assert(expected == actual) end
So, if you can decide (or remember) which particular assertion method to use, you can always use assert.
See Also
- ri Test::Unit
- The documentation for the Test::Unit library is also online at http://www.ruby-doc.org/stdlib/libdoc/test/unit/rdoc/index.html
- Recipe 15.22, "Unit Testing Your Web Site"
- Recipe 17.8, " Running Unit Tests"
- Recipe 19.1, "Automatically Running Unit Tests"
Категории