Exercise 47: Automated Testing

Having to type commands into your game over and over to make sure it's working is annoying. Wouldn't it be better to write little pieces of code that test your code? Then when you make a change, or add a new thing to your program, you just "run your tests" and the tests make sure things are still working. These automated tests won't catch all your bugs, but they will cut down on the time you spend repeatedly typing and running your code.

Every exercise after this one will not have a What You Should See section, but instead will have a What You Should Test section. You will be writing automated tests for all of your code starting now, and this will hopefully make you an even better programmer.

I won't try to explain why you should write automated tests. I will only say that you are trying to be a programmer, and programmers automate boring and tedious tasks. Testing a piece of software is definitely boring and tedious, so you might as well write a little bit of code to do it for you.

That should be all the explanation you need because your reason for writing unit tests is to make your brain stronger. You have gone through this book writing code to do things. Now you are going to take the next leap and write code that knows about other code you have written. This process of writing a test that runs some code you have written forces you to understand clearly what you have just written. It solidifies in your brain exactly what it does and why it works and gives you a new level of attention to detail.

Writing a Test Case

We're going to take a very simple piece of code and write one simple test. We're going to base this little test on a new project from your project skeleton.

First, make a ex47 project from your project skeleton. Here are the steps you would take. I'm going to give these instructions in English rather than show you how to type them so that you have to figure it out.

  1. Copy skeleton to ex47.
  2. Rename everything with NAME to ex47.
  3. Change the word NAME in all the files to ex47.

Here's me doing it:

$ cp -r skeleton ex47
$ cd ex47
$ ls
NAME.gemspec   bin             doc             lib
Rakefile       data            ext             tests
$ mv NAME.gemspec ex47.gemspec
$ mv bin/NAME bin/ex47
$ mv tests/test_NAME.rb tests/test_ex47.rb
$ mv lib/NAME lib/ex47
$ mv lib/NAME.rb lib/ex47.rb
$ find . -name "*NAME*" -print
$

That last command will find any other files with "NAME" in them so you can change them if you missed some. Refer back to Exercise 46 if you get stuck, and if you can't do this easily then maybe practice it a few times.

Next, create a simple file lib/ex47/game.rb where you can put the code to test. This will be a very silly little class that we want to test with this code in it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Room

  def initialize(name, description)
    @name = name
    @description = description
    @paths = {}
  end

  # these make it easy for you to access the variables
  attr_reader :name
  attr_reader :paths
  attr_reader :description

  def go(direction)
    return @paths[direction]
  end

  def add_paths(paths)
    @paths.update(paths)
  end

end

Once you have that file, change the unit test in tests/test_ex47.rb to this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
require "ex47/game.rb"
require "test/unit"

class TestGame < Test::Unit::TestCase

    def test_room()
        gold = Room.new("GoldRoom",
                    """This room has gold in it you can grab. There's a
                door to the north.""")
        assert_equal(gold.name, "GoldRoom")
        assert_equal(gold.paths, {})
    end

    def test_room_paths()
        center = Room.new("Center", "Test room in the center.")
        north = Room.new("North", "Test room in the north.")
        south = Room.new("South", "Test room in the south.")

        center.add_paths({'north'=> north, 'south'=> south})
        assert_equal(center.go('north'), north)
        assert_equal(center.go('south'), south)

    end

    def test_map()
        start = Room.new("Start", "You can go west and down a hole.")
        west = Room.new("Trees", "There are trees here, you can go east.")
        down = Room.new("Dungeon", "It's dark down here, you can go up.")

        start.add_paths({'west'=> west, 'down'=> down})
        west.add_paths({'east'=> start})
        down.add_paths({'up'=> start})

        assert_equal(start.go('west'), west)
        assert_equal(start.go('west').go('east'), start)
        assert_equal(start.go('down').go('up'), start)
    end
end

This file imports the Room class you made in the ex47/game.rb file so that you can do tests on it. There is then a set of tests that are functions starting with test_. Inside each test case there's a bit of code that makes a room or a set of rooms, and then makes sure the rooms work the way you expect them to work. It tests out the basic room features, then the paths, then tries out a whole map.

The important functions here are assert_equal which makes sure that variables you have set or paths you have built in a Room are actually what you think they are. If you get the wrong result, then rake test (actually Test::Unit) will print out an error message so you can figure it out.

Testing Guidelines

Follow this general set of guidelines when making your tests:

  1. Test files go in tests/ and are named test_BLAH.rb otherwise rake test won't run them. This also keeps your tests from clashing with your other code.
  2. Write one test file for each module you make.
  3. Keep your test cases (functions) short, but do not worry if they are a bit messy. Test cases are usually kind of messy.
  4. Even though test cases are messy, try to keep them clean and remove any repetitive code you can. Create helper functions that get rid of duplicate code. You will thank me later when you make a change and then have to change your tests. Duplicated code will make changing your tests more difficult.
  5. Do not get too attached to your tests. Sometimes, the best way to redesign something is to just delete it and start over.

What You Should See

$ rake test
(in /Users/zedshaw/projects/books/learn-pyrb-the-hard-way/ruby/ex47)
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -I"lib:tests" "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/rake/rake_test_loader.rb" "tests/test_ex47.rb" 
Loaded suite /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/rake/rake_test_loader
Started
...
Finished in 0.000398 seconds.

3 tests, 7 assertions, 0 failures, 0 errors

That's what you should see if everything is working right. Try causing an error to see what that looks like and then fix it.

Study Drills

  1. Go read about Ruby's Test::Unit more, and also read about alternatives.
  2. Write a new test case in tests/test_ex47.rb that creates a miniature version of your game from Exercise 45. This is one function that is similar to the current functions, but using your game's room names and abbreviated descriptions. Remember to use Room.add_paths to create the map, and use assertions to confirm everything works as expected.

Common Student Questions

I get a syntax error when I run rake test.
If you get this error then look at what the error says and fix that line of code or the ones above it. Tools like Test::Unit (which rake test runs) are running your code and the test code, so they will find syntax errors the same as running Ruby will.
I can't import ex47/game.rb?
Make sure you create the Rakefile file as specified in Exercise 46. You most likely got this wrong. You must also not be in the tests directory. Again, refer back to Exercise 46.

Video