1. Computing

Unit Testing with Minitest: Basic Assertions

By

Unit Testing with Minitest: Basic Assertions

This article is part of a series, for more information see Unit Testing with MiniTest.

Minitest is the replacement for Test::Unit as of Ruby 1.9.x. While you may continue to use Test::Unit for test-driven development in Ruby (and in fact, continue to do so often without modifying any of your code), it's important to learn about Minitest, as it offers some new features.

Installing Minitest

You already have Minitest! Yes, you can still install the Minitest gem, but it's completely unnecessary. If you have Ruby version 1.9.x or higher, you already have Minitest. However, if you are on Ruby 1.8.x, you can install Minitest with gem install minitest.

Minitest Basics

The most basic unit tests will look familiar to you if you've used Test::Unit before. A unit test script should start off by requiring minitest/autorun. Then, test suits should be encapsulated in a class that inherits from MiniTest::Unit::TestCase. Any method inside this class that begins with test_ will be considered a test to be run, plus a few more methods like setup that will act as callbacks for certain events.

So let's write a very basic test, one that simply tests the truthiness for various things in Ruby. To run these tests, simply run the script. Though we don't explicitly call anything that instantiates this class and runs the tests, that's handled by the autotest library.


#!/usr/bin/env ruby
require 'minitest/autorun'

class TestTruthiness < MiniTest::Unit::TestCase
  def test_true_and_false
    assert true
    refute false
  end
end

Here we see our first two assertions. The assert method creates an assertion that must be true. Here, true is true (obviously), so the assertion will pass. Every assertion also has an inverse, a refutation. The refute method creates an assertion that will pass if whatever is given it is false.

But this is not very helpful, if an assertion fails it should always give a useful message. Remember that one of the things test-driven development seeks to combat is stale code. If code you haven't looked at in 6 months fails, you may not know what the assertion is really testing for. While it seems obvious to you now, it may not then, or to someone not familiar with the code. So let's add messages to the assertions, and make a third assertion that fails. To add a message, pass it as the second argument to the assertion or refutation.


#!/usr/bin/env ruby
require 'minitest/autorun'

class TestTruthiness < MiniTest::Unit::TestCase
  def test_true_and_false
    assert true, "Truth is absolute"
    refute false, "False is absolute"
  end

  def test_c_like_truthiness
    refute 0, "0 is false"
  end
end

Here, we test Ruby for C-like truthiness, where 0 is false and everything else is true. In Ruby 0 is true, so this new test will fail. When you run the tests, it will tell you which test failed and give you the error message for that assertion or refutation. If we didn't have an error message, it won't even tell you exactly which assertion failed other than a line number, you then have to go digging into the tests and find it. So, again, add messages to your assertions.

To correct this incorrect assertion, we can change refute to assert.

More Complex Statements

While Minitest has more useful and semantic assertions and refutations, we're stick to assert and refute for the time being. It's rare that you'll be doing such simple assertions, so let's do something a bit more complex. In this example, we have an IP address. We must make sure that the parser we write for it correctly parses the IP address into 4 Numeric objects and on malformed IP addresses returns an empty list.

When testing your code, it's best to test all aspects of the code. Feed it good data, bad data, and a variety of data. Test for all possible outcomes (in this case, just two) to increase you code coverage. We'll talk more about code coverage later, but think of it like a workout. We must make sure the tests touch every line of the method, every branch of every conditional. If your tests don't do this, it's possible your tests will fail to catch bugs. And finally, be sure to test edge cases. It's easy to get a greater than mixed up with a greater than or equal to.


#!/usr/bin/env ruby
require 'minitest/autorun'

# Parse IP address from 1.2.3.4 into an array
# of numerics like [1,2,3,4]
# Return [] on error
def parse_ip(str)
  return [] unless str.match(/^(\d{1,3}\.){3}\d{1,3}$/)
  
  nums = str.split(/\./).map(&:to_i)
  return [] if nums.any?{|n| n < 0 || n > 255}
  return nums
end

class TestParseIP < MiniTest::Unit::TestCase
  def test_valid_ip
    assert parse_ip("1.2.3.4") == [1,2,3,4],
      "Valid IP address (1 digit)"
    assert parse_ip("10.20.30.40") == [10,20,30,40],
      "Valid IP address (2 digits)"
    assert parse_ip("100.200.100.200") == [100,200,100,200],
      "Valid IP address (3 digits)"
    assert parse_ip("100.20.3.40") == [100,20,3,40],
      "Valid IP address (mixed digits)"
  end

  def test_invalid_characters
    assert parse_ip("a.b.c.d") == [], "Non-digits"
    assert parse_ip("lacustrine yeti"), "Not an IP address"
  end

  def test_numbers_out_of_range
    assert parse_ip("1000.0.0.0") == [], "Number too high"
    assert parse_ip("-10.0.0.0") == [], "Number too low"
    refute parse_ip("0.255.0.255") == [], "Numbers on edge"
  end
end

Why Testing?

As you can see, the method itself is only a few lines long, but the tests are much longer. This is not uncommon. And while the tests themselves take many lines, they're easy to write and simple to understand. Many assume tests are a waste of time for this reason, but as Ruby such a loosely coupled language, many of the types of errors that would be caught during compile time are not caught in Ruby, or are indeed not even errors in Ruby. Yes, this is an extra step. Yes, this is more work. Yes, this is popular partially to make up for a weakness in the type of dynamic language Ruby is, but in the end you get a test suite that looks after every line of this code.

You may even ask yourself why you'd even bother testing such a simple method. What if you want to modify the method to parse port numbers as well? What if in doing that you break something? You won't know about it without tests (or angry users of your software). So whether you think unit testing is overkill or not, it's hard to deny that it's effective.

©2014 About.com. All rights reserved.