: 
http://rubylearning.com/
http://rubylearning.com/satishtalim/tutorial.html
http://rubylearning.com/download/downloads.html
http://rubylearning.com/other/certification.html
http://rubylearning.com/blog/
http://rubylearning.com/other/ruby_news.html
http://rubylearning.com/other/testimonials.html
http://rubylearning.com/jobs/ruby_jobs.html
http://rubylearning.com/other/ruby_gurus.html
http://rubylearning.com/satishtalim/services.html
http://rubylearning.com/contact/contact.html
http://rubylearning.com/satishtalim/about.html


Unit Testing
Unit testing is a method of testing your code in chunks -- typically methods. You write individual tests, which you collect into test cases. You bundle these test cases into a test suite. Because the tests are small and run quickly, you can run them frequently to ensure that your code works correctly. Most test runners (an interface for running your tests) even allow you to select a subset of your tests to run instead of running every test every time.

 

Basically, unit testing helps you write code more quickly and with more confidence. You write smaller units of code at a time, reducing the number of bugs you introduce. You see the bugs immediately as a failing test and know exactly where to look for them in the code.

 

It might seem backwards, but coding test first really does mean that you write your tests before you start writing code. Doing so gives you some boundaries for your coding: You know what to write and when to stop writing code. Ruby comes with one preinstalled, Nathaniel Talbott's Test::Unit framework that actually does most of the heavy lifting to make this process work. It creates the test runner and collects the tests into cases and suites for you.

 

A rule of thumb: Test anything that's likely to fail. You want to write tests to ensure that your method does what it's supposed to do with normal input. You should check that invalid input will be handled correctly. Finally, test your boundary conditions -- the extreme edges of your expected input.

 

The steps to writing unit tests are:
  • require 'test/unit' and set your test class to inherit from Test::Unit::TestCase
  • Write methods prefixed with test_
  • assert things you decide should be true
  • Run your tests and fix the bugs until everything passes

Here's a bare-bones testing implementation - p065my_first_test.rb

require 'test/unit'

class MyFirstTest < Test::Unit::TestCase

  def test_for_truth

    assert true

  end

end

As you can see above, your testing class (which is called a test suite) inherits from Test::Unit::TestCase and each of your tests is a method called test_that_thing_I_wanted_to_test. Within those tests, you use assertions to test whether conditions are correct in each situation.

Let's take a look at what kinds of assertions exist for you to test with. Nearly every assertion takes an optional message string, which is used to provide more information in the case of test failure.


The first group of assertions covers the values returned by the method under test:

  • assert(boolean, [message])
  • assert_equal(expected, actual, [message])
  • assert_not_equal(expected, actual, [message])
  • assert_in_delta(expected_float, actual_float, delta, [message])
  • assert_match(pattern, string, [message])
  • assert_no_match(regexp, string, [message])
  • assert_same(expected, actual, [message])
  • assert_not_same(expected, actual, [message])


The second group of assertions tests which type of object you're dealing with:

  • assert_nil(object, [message])
  • assert_not_nil(object, [message])
  • assert_instance_of(klass, object, [message])
  • assert_kind_of(klass, object, [message])


The third group of assertions covers an object's ability to respond to a method or operator:

  • assert_operator(object1, operator, object2, [message])
  • assert_respond_to(object, method, [message])
  • assert_send([obj, meth, *args], [message])


These assertions all deal with exception handling:

  • assert_raise(*args) {|| ...}
  • assert_nothing_raised(*args) {|| ...}
  • assert_throws(expected_symbol, [message], &proc)
  • assert_nothing_thrown([message], &proc)


Keeping in mind that coding test first really does mean that you write your tests before you start writing code, we now write a test suite p066testradius.rb for a simple class p067radius.rb that stores the radius of a circle.

# p066testradius.rb

require 'test/unit'

require 'p067radius'

class TestRadius < Test::Unit::TestCase

  def test_key

    robj = Radius.new('78')

    assert_equal('78', robj.key)

  end

end

Let's step through the example and see what needs to be done. Lines 1 and 2 pull in the libraries you need: Test::Unit (line 1) and radius (line 2). Lines 3-8 define the first TestCase, a class for testing Radius objects. In line 3, you're making the new class a subclass of Test::Unit::TestCase. Lines 4-7 define your first test, the test_key method of the TestRadius class.

 

The test itself consists of a bit of fixture code -- creating a radius object (robj) -- and an assertion (more on this later). This assertion just says, "The result of calling the key method on the Radius object is equal to the string 78."

 

Because you're writing your application test first, you've written these tests before you've written any code. The test suite actually forms a Ruby program on its own. When you try to run it for the first time, it will fail:


>ruby p066testradius.rb

c:/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require': no such file to load -- radius (LoadError)

      from c:/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require'

      from p066testradius.rb:2

>Exit code: 1

Fortunately, this problem is easy to fix. Simply create an empty p067radius.rb file so you can load it when you run your tests. Running the test again gives:


>ruby p066testradius.rb

Loaded suite p066testradius

Started

E

Finished in 0.0 seconds.

 

  1) Error:

test_key(TestRadius):

NameError: uninitialized constant TestRadius::Radius

    p066testradius.rb:5:in `test_key'

 

1 tests, 0 assertions, 0 failures, 1 errors

>Exit code: 1

Better, but still not quite what you want. This error message means that your test has no Radius class to use. So, define the Radius class as follows:


class Radius

end


Running your test suite again gives:


>ruby p066testradius.rb

Loaded suite p066testradius

Started

E

Finished in 0.0 seconds.

 

  1) Error:

test_key(TestRadius):

ArgumentError: wrong number of arguments (1 for 0)

    p066testradius.rb:5:in `initialize'

    p066testradius.rb:5:in `test_key'

 

1 tests, 0 assertions, 0 failures, 1 errors

>Exit code: 1

To fix these errors, you're going to have to write some actual code. When you stepped away from the test suite, it still generated errors because there wasn't a real initializer. You can fix that by writing an initializer for the object.


class Radius

  def initialize(key)

    @key = key

  end

end


Lines 2 and 3 make up the initializer method. This method takes an argument (called key) and sets an instance variable (called @key) to its value. When you run the test suite again, you get:


>ruby p066testradius.rb

Loaded suite p066testradius

Started

E

Finished in 0.0 seconds.

 

  1) Error:

test_key(TestRadius):

NoMethodError: undefined method `key' for #<Radius:0x2cc6224 @key="78">

    p066testradius.rb:6:in `test_key'

 

1 tests, 0 assertions, 0 failures, 1 errors

>Exit code: 1


You're almost there. The next step is to create the getter method key. Ruby makes this process easy. Your whole getter is shown on line 2:


class Radius

  attr_reader :key

  def initialize(key)

    @key = key

  end

end


You've written just enough code to pass your first test. The Radius class can be initialized (with a key) and has a getter method that returns the key. Now when you run your test suite, you see:


>ruby p066testradius.rb

Loaded suite p066testradius

Started

.

Finished in 0.0 seconds.

 

1 tests, 1 assertions, 0 failures, 0 errors

>Exit code: 0

Consider writing some additional tests to make sure your code doesn't fail. This method is simple, but what happens if you pass in a key that's not a String object? If it's a Fixnum object, you can just convert it to a String object. If it's anything else, your initialize should return nil. Here's what your new tests should look like:

require 'test/unit'

require 'p067radius'

class TestRadius < Test::Unit::TestCase

  def test_key

    robj = Radius.new('78')

    assert_equal('78', robj.key)

    robj = Radius.new(78)

    assert_equal('78', robj.key)

    robj = Radius.new([78])

    assert_nil(robj.key)

  end

end

You know that this code will fail, but run it anyway. You should see the following output:


>ruby p066testradius.rb

Loaded suite p066testradius

Started

F

Finished in 0.016 seconds.

 

  1) Failure:

test_key(TestRadius) [p066testradius.rb:8]:

<"78"> expected but was

<78>.

 

1 tests, 2 assertions, 1 failures, 0 errors

>Exit code: 1

The report from your last test run points out some new information. First of all, you get to see what a failure looks like, instead of an error. This failure is significant because it means that your test and code work, but not correctly. You've got a bug. Second, you see that the report shows only two assertions, even though you wrote three. The report shows only two assertions because Test::Unit won't continue past the first failure. (That way, you can focus on getting your code right one test at a time.)

 

To fix the first failure, ensure that Fixnum objects are converted to String objects by changing your code as follows:


class Radius

  attr_reader :key

  def initialize(key)

    @key = key

    if @key.class == Fixnum then

      @key = @key.to_s

    end

  end

end


Output generated by re-running your test suite indicates that you've fixed the first of two bugs:


>ruby p066testradius.rb

Loaded suite p066testradius

Started

F

Finished in 0.015 seconds.

 

  1) Failure:

test_key(TestRadius) [p066testradius.rb:10]:

<nil> expected but was

<[78]>.

 

1 tests, 3 assertions, 1 failures, 0 errors

>Exit code: 1

Now you can fix the next failure with the code by converting arrays to strings with the to_s method:


class Radius

  attr_reader :key

  def initialize(key)

    @key = key

    if @key.class == Fixnum then

      @key = @key.to_s

    end

    if @key.class != String then

      @key = nil

    end

  end

end


Now your code passes all your tests:


>ruby p066testradius.rb

Loaded suite p066testradius

Started

.

Finished in 0.0 seconds.

 

1 tests, 3 assertions, 0 failures, 0 errors

>Exit code: 0

Nearly every test you write for Radius uses the robj object. Instead of creating the object each time, you can use a setup method to do it for you. Test::Unit has two special methods: setup and teardown. 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.

Let's add a setup method to our above test suite.

def setup

  @robj = Radius.new('78')

end

def test_key

      assert_equal('78', @robj.key)

# additional functionality removed for brevity

end

The entire topic is based on the articles on this subject by Pat Eyler, Kevin Clark and the Ruby Cookbook.

An interesting read is The Power of Tests.


Assignment:
Write a test suite for a simple class called Student. This class stores a first name, a last name, and an age: a person's full name is available as a computed value.



Learning Ruby Navigation                                                                                        <Ruby/Tk  | TOC  |  Ruby Tools>