🧪🐍✨Unit Testing Python Code With The unittest Framework
Tags: python, opensource, github, beginners
Published: 2022-11-14 01:26:59

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](https://github.com/saminarp/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](https://realpython.com/python-testing/) was really useful for me to get an overview of what [unittest](https://docs.python.org/3/library/unittest.html) 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](https://realpython.com/python-testing/):

>`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:

```python
import unittest
```

2) Then, I created a class `SSGTest` that inherits from the `TestCase` class and it is meant to test my `class SSG`
```python
class SSGTest(unittest.TestCase):
```
Similarly, I have done this for my other test_parser.py file. 

```python
class CLIParser(unittest.TestCase):
```

### 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. 
```python
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")
```

`assertRaises() ` verifies that a specific exception gets raised.

```python
with self.assertRaises(SomeException):
    do_something()
```

`assertEqual()` checks for an expected result. 

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

the_exception = cm.exception
self.assertEqual(the_exception.error_code, 3)
```

## 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:

```python 
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()

```



## 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](https://docs.python.org/3/library/unittest.html): 

>Unittest supports simple test discovery. To be compatible with test discovery, all of the test files must be [modules](https://docs.python.org/3/tutorial/modules.html#tut-modules) or [packages](https://docs.python.org/3/tutorial/modules.html#tut-packages) importable from the top-level directory of the project (this means that their filenames must be valid [identifiers](https://docs.python.org/3/reference/lexical_analysis.html#identifiers)).
>Test discovery is implemented in [`TestLoader.discover()`](https://docs.python.org/3/library/unittest.html#unittest.TestLoader.discover "unittest.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](https://github.com/saminarp/rwar) :)