A Crash Course in Python Unit Testing

Harry Sauers
The Startup
Published in
5 min readApr 28, 2020
https://medium.com/feedzaitech/how-to-take-the-most-out-of-your-unit-tests-c80c88aa26cf

Unit testing is one of the most important — and overlooked — skills that a developer can have. It’s different than traditional testing of a program in a few key ways:

  1. It is automated and objective: there’s no human interaction needed, and a test case either passes or fails.
  2. It tests the smallest “unit” of a program: typically a single method, but it can be a line of code, a class, or sometimes a module.
  3. Properly written unit tests allow for “test-driven development,” a development strategy that allows developers to satisfy very specific requirements for a project.

Unit testing often isn’t even mentioned in the education of software engineers, but without it a developer runs the risk of writing bad, unreliable, or even unusable code.

Using unit testing will make you a much better developer and provide a number of benefits to your organization or project, including:

  • Improving code quality
  • Increasing development velocity
  • Making integration easier
  • Simplifying documentation
  • Find and catch bugs early
  • Many others

Python’s unit testing framework is PyUnit, essentially a Python version of Java’s widely-used JUnit. In this tutorial, we’ll write some basic tests using PyUnit for a program.

Getting Started

PyUnit comes with Python, so you don’t need to install any external packages through Pip. Open your IDE or code editor and create a new Python file called UnitTests.py. Type:

import unittest

This allows you to write unit tests for your Python methods.

Create a new Python file called TestThis.py, and write a method to test:

def multiply(num1, num2):
return num1 * num2

Now, back to your unit testing file. Import your main application file and create a testing class with a stub method:

import TestThis

class TestMethods(unittest.TestCase):
def test_multiply(self):
pass

Your First Test

Let’s write a basic test case for our multiply method:

def test_multiply(self):
result = TestThis.multiply(1, 2)
self.assertEqual(result, 2)

In order to test our program, we have to actually run the tests. Add this code to your UnitTest.py file:

if __name__ == '__main__':
unittest.main()

Run UnitTest.py. You should see this output:

Notice that we passed the test — PyUnit indicated this by the “OK” message.

However, this is only a single test case for a method. It does not cover edge cases, or inputs where we may expect it to throw an error. These are important scenarios to account for when you are writing unit tests, so always write multiple cases.

Improving Your Tests

As discussed, we need more than just a single test case.

Write a few more:

    result = TestThis.multiply(-999999999999999, 0)
self.assertEqual(result, 0)

result = TestThis.multiply(-999999999999999, 1)
self.assertEqual(result, -999999999999999)

result = TestThis.multiply(-1, -1)
self.assertEqual(result, 1)

Run your program again. It should still pass all of these cases.

Let’s write a test case where we might expect this multiply method to raise an Exception. Write a new test method where we pass in a string:

    string = "You can't have egg, bacon, sausage, and spam without the spam. "
self.assertRaises(ValueError, TestThis.multiply, string, 1)

The unittest.assertRaises method takes in an Exception, a method, and the method’s arguments as arguments. This is more or less equivalent to calling TestThis.multiply(string, 1) and returning True if it throws an exception.

Note that each test case method must begin with test_.

Give your unit tests a run!

Oh no — our test case failed! Notice that we are given a clear, concise message of what went wrong: a ValueError was not raised by multiply. This is because Python can multiply strings:

Since our multiply method isn’t intended to be using strings, let’s cast all arguments to ints so it raises the expected Exception (ValueError).

def multiply(num1, num2):
return int(num1) * int(num2)

Run your tests again. They should be successful — but keep in mind that this still isn’t a perfect unit test — you can still pass in Booleans, which may or may not have been thought of when designing this program.

Dependency Injection

Moving on to more advanced topics, dependency injection is an important concept in software engineering. Dependency injection is defined as “a technique in which an object receives other objects that it depends on.”

This technique is contrasted with simply calling an external resource from within a method, and aids in writing good unit tests that only test a single part, or unit, of your program.

Dependency injection is typically done by parameterizing dependencies, but there are other frameworks out there as well. For example, if I wanted to write a method that would print out the text of Example.com, I could write:

import requestsdef print_text(site='http://example.com'):
text = requests.get(site).text
print(text)

But, this is neither testable nor does it lack external dependencies. Notice that it has no return value — it only prints to the console — and we’re pulling in the requests library within the method.

A better way to write this would be:

import requestsdef print_text(site='http://example.com', requests=requests):
text = requests.get(site).text
print(text)
return text

Notice how we parameterize requests, and set the default value to the above requests library. You can name it something different if you feel removing ambiguity would add value, as well. This prevents us from testing something else, i.e. the requests library or your Internet connection, when running a unit test for this method.

We also added a return value to this function. This allows us to programmatically test it — so let’s get to it.

First, we need to mock, or spoof, the requests library. Take a look inside the structure of the method call: requests.get(site).text

requests.get(site) returns a Response object, which has an attribute text. This is all the functionality we need to mock, so write a couple basic classes:

class Response:
def __init__(self):
self.text = 'Your mother was a hamster!'

class Requests:
def __init__(self):
pass

def get(self, url):
return Response()

Now, write up a unit test that uses these classes instead of the actual requests library.

def test_print_text(self):
expected_result = 'Your mother was a hamster!'
result = TestThis.print_text('Any string is fine here', Requests())
self.assertEqual(result, expected_result)

Run your tests — you should see output like this:

Congratulations! You successfully used dependency injection in a unit test.

Obviously, there’s lots more to learn about unit testing in Python and in general, but I hope this provided a good foundation in writing better, testable code.

Please feel free to leave some feedback or questions, or reach out to me on LinkedIn or harry@hsauers.net!

--

--

Harry Sauers
The Startup

Writing about software, cybersecurity, and finance.