Introduction To Unit Testing In Python & Ruby (Posted on May 25th, 2013)

One of the things I noticed that isn't really taught much is creating test suites for your programs. A lot of times you'll just have sample input and output to test your programs. This works great for one off applications but doesn't really carry over to on-going application development life cycle, especially when new developers are constantly being added to a project.

You don't need any crazy library to do testing. In fact Python and Ruby come with their own testing libraries. I find it's always nice to use built-in library functions to achieve certain tasks. If you follow my blog you may have read my Introduction to Graph Theory article which has Python and Ruby code for Dijkstra's algorithm. If you're interested in other languages you can checkout the git repo. For the purposes of this article we'll be using the Python and Ruby implementations mentioned in the article. You can find somewhat updated versions in the git repos. The Python version has been updated to 3.x and both versions now won't print out shortest path solutions when tests are run.

What We're Testing

Outside of our initialize and to string methods we have two other methods that help us find the shortest path in a graph, add_vertex and shortest_path. These are the two functions we're going to write unit tests for. In case you're unfamiliar with the terminology, unit testing is a type of testing used for testing individual blocks of code such as a single function or procedure. There's no need to over think it. We're simply sending test values to a function to make sure we get back the result we expect.

Our goals will be to make sure that when we add a vertex to the graph that all the edges are added properly, and that when we test for the shortest path we find it if it exists or not.

Initializing Our Test Suite

Before we begin I just wanted to point you to the documentation for Python unit testing and Ruby unit testing. You shouldn't need them to get through this article but they do provide a lot of information about testing.

Our first step is importing our testing library along with the Dijkstra's sample code. A common feature among unit testing libraries is to have a setup which runs before each test. This can be useful if you have a set of code that is used by many tests. For this set of tests we'll need our graph class for each test. So we'll create a graph object that will be created for each test.

Python

import unittest
from dijkstras import Graph

class Graph_Test(unittest.TestCase):
    
    #Runs before each test
    def setUp(self):
        self.graph = Graph()

unittest.main()

Ruby

If you're here for Ruby you'll notice that the linked docs are for 1.8.7. This is because most of the example code has disappeared for future versions but the library pretty much works the same. Due to the sparse documentation of this library a lot of other third-party libraries have become popular such as RSpec. The Ruby examples I show will use the built-in library, but as a challenge feel free to install the RSpec gem and implement the tests for this code or from some of your other code.

require 'test/unit'
require_relative 'dijkstras'

class Graph_Test < Test::Unit::TestCase
    
    #Runs before each test
    def setup
        @graph = Graph.new
    end
end

Testing add_vertex

add_vertex is an interesting function because it has no return value. So we have a few options as to the approach we can take for this test. One option is to add a return value. However, it's not really clear what would be best to return. Perhaps the vertex, maybe all vertices, or just a true/false value. Another option would be to add a getter function to try and get the edges of a vertex that we pass to the function. This adds another point of failure into our program but is still a viable option depending on your programming style. My idea is to just access the vertices instance variable. This will allow us to see if our newly added vertex is in the graph. This is kind of like the getter method but without having to add a new function.

The way unit testing works is you make a function call and check the value it returns against what you expect it to return. Usually you'll want to check if the returned value equals the value you expect. Most testing suites will provide a way to assert if something is equal or not. This is how we'll be testing if our vertices variable is equal to the value we expect it to be.

Python

import unittest
from dijkstras import Graph

class Graph_Test(unittest.TestCase):
    
    #Runs before each test
    def setUp(self):
        self.graph = Graph()
    
    def test_add_vertex(self):
        self.graph.add_vertex('A', {'B': 7, 'C': 8})
        self.assertEqual(self.graph.vertices, {'A': {'C': 8, 'B': 7}})
    
unittest.main()

Ruby

require 'test/unit'
require_relative 'dijkstras'

class Graph_Test < Test::Unit::TestCase
    
    #Runs before each test
    def setup
        @graph = Graph.new
    end
    
    def test_add_vertex
        @graph.add_vertex('A', {'B' => 7, 'C' => 8})
        assert_equal({"A"=>{"B"=>7, "C"=>8}}, @graph.instance_variable_get(:@vertices), "Failed to add vertex")
    end
end

You can see what a failed test looks like by changing one of the values in the expected clause. So if you change the value of C to 3 you should see an error message that tells you which test failed. It will also show you the returned value versus what was expected. This can be super helpful in debugging errors.

Testing shortest_path

Testing our shortest path algorithm is a little more straight forward since we have a return value. However, one thing we have to take in to consideration is the fact that we have two different return statements, one for when a path is found and one for when no path is found. This means we need to create two tests. Once again we have a couple of options as to how we can achieve this. One option is to create two test functions to test both return paths. Another option is to create one test but use two assert statements. Neither is really more correct than the other so I've chosen to use two assert statements inside of one test function.

Since we are creating a new function our setup function is going to run again. This means we'll be starting with a fresh new graph object. So our first step will be to insert some nodes into the graph so that we can try and find a path. Once we do that we simply call our shortest_path function as we normally would and tell our assert function what to expect in return. Here's a picture of the graph for reference:

Visual representation of the computer science graph that's being implemented

Python

import unittest
from dijkstras import Graph

class Graph_Test(unittest.TestCase):
    
    #Runs before each test
    def setUp(self):
        self.graph = Graph()
    
    def test_add_vertex(self):
        self.graph.add_vertex('A', {'B': 7, 'C': 8})
        self.assertEqual(self.graph.vertices, {'A': {'C': 8, 'B': 7}})
        
    def test_shortest_path(self):
        self.graph.add_vertex('A', {'B': 7, 'C': 8})
        self.graph.add_vertex('B', {'A': 7, 'F': 2})
        self.graph.add_vertex('C', {'A': 8, 'F': 6, 'G': 4})
        self.graph.add_vertex('D', {'F': 8})
        self.graph.add_vertex('E', {'H': 1})
        self.graph.add_vertex('F', {'B': 2, 'C': 6, 'D': 8, 'G': 9, 'H': 3})
        self.graph.add_vertex('G', {'C': 4, 'F': 9})
        self.graph.add_vertex('H', {'E': 1, 'F': 3})
        
        self.assertEqual(self.graph.shortest_path('A', 'H'), ['H', 'F', 'B'])
        self.assertEqual(self.graph.shortest_path('H', 'I'), {'A': 12, 'B': 5, 'C': 9, 'D': 11, 'E': 1, 'F': 3, 'G': 12, 'H': 0})
    
    
unittest.main()

Ruby

require 'test/unit'
require_relative 'dijkstras'

class Graph_Test < Test::Unit::TestCase
    
    #Runs before each test
    def setup
        @graph = Graph.new
    end
    
    def test_add_vertex
        @graph.add_vertex('A', {'B' => 7, 'C' => 8})
        assert_equal({"A"=>{"B"=>7, "C"=>8}}, @graph.instance_variable_get(:@vertices), "Failed to add vertex")
    end
    
    def test_shortest_path
        @graph.add_vertex('A', {'B' => 7, 'C' => 8})
        @graph.add_vertex('B', {'A' => 7, 'F' => 2})
        @graph.add_vertex('C', {'A' => 8, 'F' => 6, 'G' => 4})
        @graph.add_vertex('D', {'F' => 8})
        @graph.add_vertex('E', {'H' => 1})
        @graph.add_vertex('F', {'B' => 2, 'C' => 6, 'D' => 8, 'G' => 9, 'H' => 3})
        @graph.add_vertex('G', {'C' => 4, 'F' => 9})
        @graph.add_vertex('H', {'E' => 1, 'F' => 3})
        
        assert_equal(['H', 'F', 'B'], @graph.shortest_path('A', 'H'), 'Failed to find shortest path')
        assert_equal({'A' => 12, 'B' => 5, 'C' => 9, 'D' => 11, 'E' => 1, 'F' => 3, 'G' => 12, 'H' => 0}, @graph.shortest_path('H', 'I'), 'Failed to find shortest paths from root node')
    end
end

That's really all there is to unit testing. If you're doing web development definitely check with the framework you're using to see if they have a recommended testing suite. If you're rolling your own framework there are many testing frameworks that can help you out. Most of them will work the same from language to language.

The Github repo for Dijkstra's algorithm has been updated with the newest testing code. Definitely feel free to check it out and make pull requests if you haven't already.

As always if you have any feedback or questions feel free to drop them in the comments below or contact me privately on my contact page. Thanks for reading!

Tags: Python, Ruby

Comments:

  • There are currently no comments. You can be first!