How do I test my code with Minitest?
28/Jul 2011
How do I test my code with Minitest?
This guest post is by Steve Klabnik, who is a software craftsman, writer, and former startup CTO. Steve tries to keep his Ruby consulting hours down so that he can focus on maintaining Hackety Hack and being a core member of Team Shoes, as well as writing regularly for multiple blogs.
Programming is an interesting activity. Everyone has their favorite metaphor that really explains what programming means to them. Well, I have a few, but here’s one: Programming is all about automation. You’re really just getting the computer to automatically do work that you know how to do, but don’t want to do over and over again.
When I realized this, it made me look for other things that I do that could be automated. I don’t like repeating myself over and over and over again. That’s boring! Well, there’s one particular task that’s related to programming that’s easily made automatic, and that’s testing that your software works!
Does this story sound familiar? You run your program, try a few different inputs, check the outputs, and see that they’re right. Then, you make some changes in your code, and you’d like to see if they work or not, so you fire up Ruby and try those inputs again. That repetition should stick out. There has to be a better way.
Luckily, there is! Ruby has fantastic tools that let you set up tests for your code that you can run automatically. You can save yourself tons of time and effort by letting the computer run thousands of tests every time you make a change to your code. And it’ll never get tired and accidentally type in a 2 when you mean to type 3… Many people take this one step farther. They find testing so important and so helpful that they actually write the tests before they write the code! I won’t expound on the virtues of “test driven development” in this post, but it’s actually easier to write the tests first, once you get some practice at it. So, let’s pick a tiny bit of code to work on, and I’ll show you how to test it using Ruby’s built-in testing library, minitest.
For this exercise, let’s do something simple, so we can focus on the tests. We’ll make a Ruby class called CashRegister. It’ll have a bunch of features, but here’s the first two methods we’ll need:
- The register will have a scan method that takes in a price, and records it.
- The register will have a total method that shows the current total of all the prices that have been scanned so far.
- If no prices have been scanned, the total should be zero.
- The register will have a clear method that clears the register of all scanned items. The total should go back to zero again.
Seems simple, right? You might even know how to code this already. Sometimes, intermediate programmers practice coding problems that are easy, just to focus on how to write good tests, or to work on getting the perfect design. We call these kinds of problems ‘kata.’ It’s a martial arts thing.
Anyway, enough about all of this! Let’s dig in to minitest. It already comes with Ruby 1.9, but if you’re still using 1.8, you can install it with ‘gem install minitest
.’ After doing so, open up a new file, register.rb, and put this in it:
require 'minitest/autorun' class TestCashRegister < MiniTest::Unit::TestCase def setup @register = CashRegister.new end def test_default_is_zero assert_equal 0, @register.total end end
Okay! There’s a lot going on here. Let’s take it line by line. On the first line, we have a ‘require.’ The autorun part of minispec includes everything you need to run your tests, automatically. All we need to do to run our tests is to type ruby register.rb, and they’ll run and check our code. But let’s look at the rest of the file before we do that. The next thing we do is set up a class that inherits from one of minitest’s base classes. That’s how minitest works, by running a series of TestCases. It also lets you group similar tests together, and split different ones up into multiple files.
Anyway, enough organizational stuff. In this class, we have two methods: the first is the setup method. This runs before each test, and allows us to prepare for the test we want to run. In this case, we want a new CashRegister each time, and we’ll store it in a variable. Now we don’t have to repeat our setup over and over again… it’s just automatic!
Finally, we get down to business, with the test_default_is_zero
method. Minitest will run any method that starts with test_ as a test. In that method, we use the assert_equal
method with two arguments. assert_equal
is where it all happens, by comparing 0 to our register’s total, and it will complain if they’re not equal.
Okay, so we have our first test. Rock! You might be tempted to start implementing our CashRegister class, but wait! Let’s try running the tests first. We know they’ll fail, because we don’t even have a CashRegister yet! But if we run the tests before writing code, the error messages will tell us what we need to do next. The tests will guide us through the implementation of our class. So, as I mentioned earlier, we can run the tests by doing this:
$ ruby register.rb
We get this as output:
Loaded suite register Started E Finished in 0.000853 seconds. 1) Error: test_default_is_zero(TestRegister): NameError: uninitialized constant TestRegister::CashRegister register.rb:5:in `setup' 1 tests, 0 assertions, 0 failures, 1 errors, 0 skips Test run options: --seed 36463
Whoah! Okay, so you can see that we had one test, one error. Since we know classes are constants in Ruby, we know that the uninitialized constant error means we haven’t defined our class yet! So let’s do that. Go ahead and stick in an empty class at the bottom:
class CashRegister end
And run the tests again. You should see this:
1) Error: test_default_is_zero(TestRegister): NoMethodError: undefined method `total' for #<CashRegister:0x00000101032a80> register.rb:9:in `test_default_is_zero'
Progress! Now it says we don’t have a total method. So let’s define an empty one. Modify the class like this:
class CashRegister def total end end
And run the tests again. Another failure:
1) Failure: test_default_is_zero(TestRegister) [register.rb:9]: Expected 0, not nil.
Okay! No more syntax errors, just the wrong result. Let’s keep it as simple as possible, and fill out a nice and easy total method:
def total 0 end
Now, you may be saying, “Steve, that doesn’t calculate a total!” Well, you’re right. It doesn’t. But our tests aren’t yet asking to calculate a total, they’re just asking for a default. If we want a total, we should write a test that actually demonstrates adding it up. But we have fulfilled objective #3, so we’re doing good! Now, let’s work on objective #2, since we sorta feel like the total method is lying about what it’s supposed to do. In order to add up the items that were scanned, we need to scan them in the first place! Objective #1 says that this method should be called scan, so let’s write a test. Put it in your test class with the test_default_is_zero method:
def test_total_calculation @register.scan 1 @register.scan 2 assert_equal 3, @register.total end
Make sense? We want to scan two things in, and then check that the total is correct. Let’s run our tests!
Loaded suite register Started .E Finished in 0.000921 seconds. 1) Error: test_total_calculation(TestRegister): NoMethodError: undefined method `scan' for #<CashRegister:0x00000101031838> register.rb:13:in `test_total_calculation' 2 tests, 1 assertions, 0 failures, 1 errors, 0 skips Test run options: --seed 54501
Okay! See that ‘.E’ up there? That graphically shows that we had one test passing, and one test with an error. Our first test still works, but our second is failing because we don’t have a scan method. Add an empty one to our CashRegister class, and run again:
1) Error: test_total_calculation(TestRegister): ArgumentError: wrong number of arguments (1 for 0) register.rb:24:in `scan' register.rb:13:in `test_total_calculation'
Whoops! It takes an argument. Let’s add that: def scan(price). Run the tests!
1) Failure: test_total_calculation(TestRegister) [register.rb:15]: Expected 3, not 0.
Okay! This sounds more like what we expected. Our total method just returns zero all the time! Let’s think about this for a minute. We need to have scan add the price to a list of scanned prices. So we’d better have it do that:
def scan(item) @items << item end
But if you run the tests, you’ll see this:
1) Error: test_total_calculation(TestRegister): NoMethodError: undefined method `<<' for nil:NilClass register.rb:25:in `scan' register.rb:13:in `test_total_calculation'
Oops! @items is undefined. Let’s make it be an empty array, when we create our register:
def initialize @items = [] end
And run the tests:
1) Failure: test_total_calculation(TestRegister) [register.rb:15]: Expected 3, not 0.
Okay! We’re back to our original failure. But we’ve made some progress: now that we have an actual list of items, we’re in a position to make our total method work. Also, at each step, even though one test was failing, the other was still passing, so we know that we didn’t break our default functionality while we were working on getting a real total going.
Now, we’re in a better place to calculate the total:
def total @items.inject(0) {|sum, item| sum += item } end
Or, if you want to make it even shorter:
def total @items.inject(0, &:+) end
If you’re not familiar with Enumerable#inject
, it takes a list of somethings and turns it into a single something by means of a function, in a block. So in this case, we can keep a running sum of all items, and then add the price of each one to the sum. Done! Run your tests:
Started .. Finished in 0.000762 seconds. 2 tests, 2 assertions, 0 failures, 0 errors, 0 skips
Woo hoo! We’re done! Our total can now be calculated. Great job!
Now, here’s a challenge, to see if you’ve really learned this stuff: write a test for a new method, clear, that clears the total. That’s objective #4 we talked about above.
Other parts of minitest
This has been a mini intro to minitest and using it to test your code. There are other methods in the assert
family, too, like assert_match
, which takes a regular expression and tries to match it against something. There’s the refute
family of tests, which are the opposite of assert
:
assert true #=> pass refute true #=> fail
There are also other tools that make minitest useful, like mocks, benchmark tests, and the RSpec-style ‘spec’ syntax. Those will have to wait until later! If you’d like to learn about them now, check out the source code on GitHub.
Happy testing!
I hope you found this article valuable. Feel free to ask questions and give feedback in the comments section of this post. Also, do check out Steve’s other article: “How do I keep multiple Ruby projects separate?” on RubyLearning. Thanks!