DEV Community

Cover image for 🧪🐍✨Unit Testing Python Code With The unittest Framework
Samina Rahman Purba
Samina Rahman Purba

Posted on

🧪🐍✨Unit Testing Python Code With The unittest Framework

Unit testing is an important part of software development as it isolates different components of a software program or system and checks if it operates the right way. It ensures that the code meets quality standards and that flaws or bugs in a system can be properly traced back to the isolated unit of code that is failing, and remedy the failure promptly.

My static site generator - rwar is evolving and getting better over time, which means I need to ensure that the code is professional and of high quality! Also, being able to work with testing frameworks and learning them well can help you stand out during interviews. 😃

The learning curve 📚🧠

I was not used to writing tests much for the personal projects and small group projects I have worked on so far, which is why thinking of code from a test-driven perspective was challenging for me. I could feel my neurons firing as I was trying hard to think how to put all the unit testing pieces together. Until now, I only had a little bit of experience working with the Jest testing framework for JavaScript, and Junit for Java, so I had to go through a big learning curve to understand the unittest framework. This explanation was really useful for me to get an overview of what unittest is and how to get started with it. I used to underestimate the importance of reading documentation, but I know now how crucial that skill is.

Why unittest framework❓

Upon doing some research on some of the most popular testing frameworks for Python I came across pytest, nose, and unittest. They are all great tools, however, I decided to give unittest a try for this project because unittest was inspired by JUnit which I had some prior experience with. It is always good to experiment and learn different frameworks to eventually find out what we like most. I am going to give pytest a try for my next Python project. Each testing framework has its conventions for naming files and structuring code, which is another important reason to look at examples and go through the documentation for the framework of your choice.

Having a good test plan 📝

It is really important to have a good test plan before starting.

1) Understanding what your code does

  • What kind of inputs the function takes
  • What are the program's functions
  • What are the expected outputs

2) Good and bad test scenarios

  • Think of the success cases and cases that are anomalies. For example, if you are building a school management system - adding the same student twice should result in some sort of warning.

3) What does not need testing

  • A good example of what does not need testing would be functions from external packages. Another example would be a program invoking functions where the code was already tested.

4) Writing the tests

  • Then, comes the test writing part!
  • For example, if you are building a school management system - then you can test for functions such as adding students, loading an existing student, adding the same student twice, and so on.

The process 🔨

For my project I have created two test files - test_parser.py and test_ssg.py - under the test folder.

As stated in this documentation:

unittest requires that:

  • You put your tests into classes as methods
  • You use a series of special assertion methods in the unittest.TestCase class instead of the built-in assert statement

1) To get started with unittest I first had to:

import unittest
Enter fullscreen mode Exit fullscreen mode

2) Then, I created a class SSGTest that inherits from the TestCase class and it is meant to test my class SSG

class SSGTest(unittest.TestCase):
Enter fullscreen mode Exit fullscreen mode

Similarly, I have done this for my other test_parser.py file.

class CLIParser(unittest.TestCase):
Enter fullscreen mode Exit fullscreen mode

An example:

It is hard to go over all the test I have written for my project because there are many! However,
here is an example from test_parser.py. Notice, how I have used descriptive names for my test. Here I am checking if the test has any input files or not.

class CLIParser(unittest.TestCase):

def test_without_input(self):

with self.assertRaises(SystemExit) as err:

get_parser_args([])

self.assertEqual(err.exception.code, 2, "No input directory provided")
Enter fullscreen mode Exit fullscreen mode

assertRaises() verifies that a specific exception gets raised.

with self.assertRaises(SomeException):
    do_something()
Enter fullscreen mode Exit fullscreen mode

assertEqual() checks for an expected result.

with self.assertRaises(SomeException) as cm:
    do_something()

the_exception = cm.exception
self.assertEqual(the_exception.error_code, 3)
Enter fullscreen mode Exit fullscreen mode

When to use setUp() and tearDown() ❓👀

If you go through my code on test_ssg.py, you will see that I have used the setUp() and tearDown() methods. For every test in test_ssg.py, a temporary file was created which needed to be deleted after the test, which is what I could accomplish with setUp() and tearDown(). These were repetitive processes that I could simplify using these methods. Imagine you have a suite with 10 tests and 7 of them require the same setup/teardown code, while the 3 don't, setup and teardown gives you a nice way to refactor the code. It is often used when we need to create a fake database for testing purposes or for mocking purposes.

Here is an example of how I have used it in my code:

class SSGTest(unittest.TestCase):

def setUp(self):

self.tempdir = tempfile.TemporaryDirectory()


self.output = self.tempdir.name

self.input = os.path.join(self.output, "input")

os.mkdir(self.input)

self.stylesheet = "test-stylesheet-link"


def tearDown(self):

self.tempdir.cleanup()

Enter fullscreen mode Exit fullscreen mode

Test discovery 💡

Test discovery is the process of locating where the tests are in your codebase. This means that you don't have to explicitly state where the tests are located as the testing framework can automatically locate them based on the matching patterns of the naming convention. In this case, it will look for test*.py `files.

As stated in the documentation:

Unittest supports simple test discovery. To be compatible with test discovery, all of the test files must be modules or packages importable from the top-level directory of the project (this means that their filenames must be valid identifiers).
Test discovery is implemented in TestLoader.discover(), but can also be used from the command line. The basic command-line usage is:


cd project_directory
python -m unittest discover

Upon running that command, it shows the number of passing and failing tests. I had two tests that were failing, so I went back and fixed those before pushing my changes to GitHub.

Documentation ✏️

A good software or system is properly documented. I used to underestimate the value of documentation until I participated in Hacktoberfest and had to work on large open-source projects. Some interesting projects I wanted to contribute to had unclear documentation and even though I wanted to work on them, I just couldn't. The projects I ended up contributing to were all properly structured and had clear documentation.

The same goes for rwar. I am trying to write clear documentation so that anyone interested in using it or contributing to it will find it easy to do so. I have added the instructions for testing on the CONTRIBUTING.md file!

For the full codebase, please feel free to check it out on GitHub :)

Top comments (2)

Collapse
 
batunpc profile image
Batuhan Ipci

Very informative! 👾

Collapse
 
saminarp profile image
Samina Rahman Purba

Thank you!