Why and How: Ruby (and Rails) Unit Testing

Posted by kev Sun, 30 Oct 2005 05:10:00 GMT

One thing I love about Ruby is the built in unit tests. As someone who is working to use better developing methodologies, having unit testing avaliable with no effort makes it that much easier to try out things like Test Driven Development. Unit testing also has an important place in Rails development. I enjoyed the format of my article on migrations, so we’ll try that again. The why and how of unit testing, coming right up.

Why

  • It means never having to say “I created a bug while fixing another one”
  • It means you don’t have to run your program and play with it (which is slow) in order to find errors. Unit testing is much much faster than testing manually.
  • Knowing how to use unit tests opens up the world of test driven development.

How

Summarized summary

  • 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.

‘In depth’ summary

Now for the fun. Whenever I find a new feature in ruby it tends to be drastically easier than I expected and it makes me happy. Unit testing is one of those cases. Here’s a bare bones testing implementation in ruby:

my_first_test.rb
require 'test/unit'

class MyFirstTest < Test::Unit::TestCase
  def test_for_truth
    assert true
  end
end

Really, all of this is covered in the summarized summary. 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. You can find the available ruby assertions here, and for rails folks the core team has extended Test::Unit and those docs can be found in the api.

With that out of the way, lets take a look at examples.

Simple example: Arithmetic is fun

Lets say we’ve written a simple class, MyMath, which implements basic arithmetic. MyMath’s run method takes an expression and returns a result. We would want several tests to show addition, subtraction, multiplication and division work. The positive unit tests for addition and subtraction (to show things work) might look like this:

my_math_test.rb
require 'my_math'
require 'test/unit'

class MyMathTest < Test::Unit::TestCcase
  def test_addition
    assert_equal 4, MyMath.run("2+2")
    assert_equal 4, MyMath.run("1+3")
    assert_equal 5, MyMath.run("5+0")
    assert_equal 0, MyMath.run("-5 + 5")
  end

  def test_subtraction
    assert_equal 0, MyMath.run("2-2")
    assert_equal 1, MyMath.run("2-1")
    assert_equal -1, MyMath.run("2-3")
  end
end

To run tests, you simply run your testing files in ruby, eg. ruby my_math_test.rb. The output will be a series of ‘.’s, ‘E’s and ‘F’s. Each indicates an assertion being run, with ‘.’s as passing, ‘E’ as a runtime error and ‘F’ as a failure. In the end it will give you a summary which looks like this:

[Dionysus:~/code/mymath] kevincla% ruby test/my_math_test.rb 
Loaded suite test/my_math_test
Started
.......
Finished in 0.015931 seconds.

7 tests, 13 assertions, 0 failures, 0 errors

In addition to positive tests, you will also want to write negative unit tests which try to break your code. This might include testing for exceptions being raised when using input like MyMath.run("a + 2") or MyMath.run("4/0"). Those might look something like this (just the methods this time):

  def test_for_non_numerics
    assert_raises(NonNumericError) do
      MyMath.run("a + 2")
    end
  end

  def test_division_by_zero
    assert_raises(DivisionByZeroError) do
      MyMath.run("4/0")
    end
  end

For the Railers in the audience

Rails has two types of testing schemes, unit and functional. They follow the same basic principles as other Ruby testing, but are a bit more specific.

In Rails, unit testing is specifically for models. When you write special functions for a model you should test it with unit tests. For example, the Rails login generator creates unit tests for your User model to make sure that authentication works and that bad passwords aren’t accepted.

Functional tests in Rails are for testing your controllers. In these, you might test that when someone goes to the login screen that they are prompted for a username and password.

For more information on Rails testing, I recommend A Guide to Testing the Rails.

Another useful tidbit

Sometimes you need to have things happen before and after each tests. The setup and teardown methods are your cohorts in this venture. Any code you write in setup will be run before each test and code in teardown will be run afterwards.

Automating your tests with Rake

If you’re writing tests for all your code (as you should be), the number of testing files begins to grow. One thing that will make life easier is automating your tests and Rake is probably the right tool for the job. Using Rake is the subject for a whole different article, but I’ll leave you with a snippet that makes life easier for me.

Rakefile
require 'rake'
require 'rake/testtask'

task :default => [:test_units]

desc "Run basic tests"
Rake::TestTask.new("test_units") { |t|
  t.pattern = 'test/*_test.rb'
  t.verbose = true
  t.warning = true
}

Basically, a Rakefile defines tasks that rake can perform. In my Rakefile, the default task (the one that happens when you just run rake in a directory with a Rakefile in it) is set to my unit tests task. In my task “test_units”, rake is configured to look for files in the test directory that end with _test.rb and run them. This means you can put all your tests in your testing directory and let rake go to work.

Conclusivelike section

Testing that things work as expected is good. Make sure to do it whenever possible: It just plain makes your code better. There is a thing as having too many tests, however, so remember that you want to test that things work and that they don’t break. Don’t write 700 assertions when 10 will have the same coverage.

Oh, and don’t forget that a test suite is just a Ruby class. Feel free to write any helper methods inside the class and then use them to run test cases. If you’re feeling really intense you can even generate input for those tests in helpers, but again, that is for another article and I’m rather sleepy.

Posted in , ,  | 7 comments | no trackbacks

Comments

  1. Avatar Damien Tanner said about 19 hours later:

    Thanks mr kc.

  2. Avatar perica said 35 days later:

    very nice info.

    thanks

  3. Avatar Axel said 79 days later:

    hey, finally an working example fur Text::Unit many thanks!

  4. Avatar Andre said 92 days later:

    I kept getting an error until I included the my_math.rb file in the test file. The article implies that the code in my_math_test.ruby will automagically find a file containing the class MyMath (maybe look for it based on the class name?)

    Is there such a mechanism in ruby and what are the naming conventions? OR, did the article unintentionally omit this detail?

    Thank you.

    Here’s the error I kept getting until I added

    require ‘my_math.rb’

    at the top of test_my_math.rb file.

    >ruby test_my_math.rb Loaded suite test_my_math Started E Finished in 0.0 seconds.

    1) Error:

    test_addition(MyMathTest): NameError: uninitialized constant MyMathTest::MyMath test_my_math.rb:11:in `test_addition’

    1 tests, 0 assertions, 0 failures, 1 errors >Exit code: 1

  5. Avatar Kevin Clark said 92 days later:

    Andre: The article unintentionally omited the require. I’m adding it now.

  6. Avatar Nathaniel Brown said 121 days later:

    Great article. Had previously learned how to write tests, but it has been awhile and this was a great refresher.

    Many Thanks!

  7. Avatar vijay ramanan said 150 days later:

    Thanks for the info and the link to the rails testing manual..I found your articles very helpful..The one on joy of migrations is also very informative..I love this framework already..

Trackbacks

Use the following link to trackback from your own site:
http://glu.ttono.us/articles/trackback/79

Comments are disabled