Testing Python Code Using UnitTest
Tags: python, beginners
Published: 2023-11-09 18:49:30

Hey everyone, I recently had to create testers for my [TXT to HTML Converter](https://github.com/Pasqua101/txt-to-HTML-converter) program. To do this I used the built-in UnitTest framework, which I'll talk about below.

### Why did I Choose the UnitTest Library?
Like I said above, I chose the [UnitTest](https://docs.python.org/3/library/unittest.html) framework because it is built into Python, which should make things easier for contributors as it reduces the number of libraries they need to install in order to get started on the program. I also have a little experience with reading the syntax for the library due to a Data Structures and Algorithms class I took in a previous semester, where my professor had us write data structures and algorithms in Python which we had to test with testers they wrote with the UnitTest framework.

Aside from that the UnitTest framework is quite simple to understand and works very well with my IDE, [PyCharm](https://www.jetbrains.com/pycharm/), since I can run the tests in the IDE without having to use the command-line. I'll talk about the setup and how I got them to work with my program next

### Using UnitTest with my Program
To set up the framke work, I had to import the unit test library into a python file that starts with `test_`, as well as the function you want to test. Then you must define a class that uses `unittest.TestCase` and create a function in the class that also has the `test_` prefix. To better explain it, I'll show a brief example below for a function I have that returns true or false based on the file extension.
```python
import unittest

from helper import extension_checker


class TestExtensionChecker(unittest.TestCase):
    def test_extension_checker_txt(self):
        result = extension_checker("1.txt")
        self.assertTrue(result)
        
        result = extension_checker("1.md")
        self.assertTrue(result)
        
        result = extension_checker("1.html")
        self.assertFalse(result)


if __name__ == "__main__":
    unittest.main()
```
Using self.assertTrue or False allows the tester to make sure that the return value is what it is supposed to be. There is also self.assertEqual which you can use to directly compare the return value with a string or another value. An example is provided below;

```python
class TestMarkdown(unittest.TestCase):
    def test_convert_bold(self):
        converted_string = parse_md("**Hello World**")
        self.assertEqual(converted_string, "<strong>Hello World</strong>")

    def test_convert_hr(self):
        converted_string = parse_md("---")
        self.assertEqual(converted_string, "<hr>")

    def test_convert_link(self):
        converted_string = parse_md("[YouTube](https://www.youtube.com/)")
        self.assertEqual(
            converted_string, "<a href=https://www.youtube.com/>YouTube</a>"
        )

    # Tests conversion result for both 1 and 3 backticks
    def test_convert_code(self):

        converted_string = parse_md("`\``Hello world`\``")
        # Note: In my actual code the backslash isn't in the string. 
        # I did this to prevent Markdown from reading "Hello world" as another code block.
        
        self.assertEqual(converted_string, "<code>Hello world</code>")

        converted_string = parse_md("`Hello world`")
        self.assertEqual(converted_string, "<code>Hello world</code>")
```
It was with this test that I made that I was able to test my `parse_md` function, previously called `check_md_and_write`, and locate a bug that I uncovered a last week. I noticed this bug when I was using the linter, [Ruff](https://docs.astral.sh/ruff/), and formatter, [Black](https://black.readthedocs.io/en/stable/), I set up for my project. If you're interested in reading about the linter and formatter I chose and the setup process you can read [last week's blog](https://dev.to/pasqua101/picking-out-a-formatter-and-linter-for-my-open-source-project-1gj2). Essentially the problem was that I could not parse any Markdown in my program. I wasn't sure what the problem was, but I think it had something to do with when I refactored my code and tried to clean things up. Luckily, I still has the branches where I worked on improved the function to parse markdown and the refactoring branch. To make note of it, I made an [issue](https://github.com/Pasqua101/txt-to-HTML-converter/issues/21) for myself and specified which branches to take a look at. 

To fix the solution I compared the code I had with the branch I made for [issue-16](https://github.com/Pasqua101/txt-to-HTML-converter/issues/16) since this had the completed version of my function, just without the refactoring changes. I ended up falling back on this function and changed the way it gets called. Which was by checking if the file had the md extension before entering the function, whereas before I would enter the function and then check if it was an md file. I did this because when I ran the tester I noticed that it would not return anything, so I suspected that it wasn't able to check the file extension and would return nothing as a result. This change worked and got the function working properly again. I made sure to commit this just in case, I messed up the function again in the future.

The next tester I made was for my main program logic. This one was a little more complex for me, as I was initially stuck on how to use it and thought I had to make mock files and folders to get it work. However, it was simpler than I thought. Unittest has two functions that worked for my program, called [setUp](https://docs.python.org/3/library/unittest.html#unittest.TestCase.setUp) and [tearDown](https://docs.python.org/3/library/unittest.html#unittest.TestCase.tearDown). `setUp` is called before the test is executed and is meant to prepare the test call for the test function. `tearDown` is called once the test ends and cleans up any files or folders that were made from the test. Using these two functions I was able to build this test to test the result of txt to html conversion;

```python
import os
import tempfile
import unittest

from file_processors import text_to_html

class TestTextToHtmlTxtConversion(unittest.TestCase):
    def setUp(self):
        # Creating a temporary input file with some text content
        self.input_file = tempfile.NamedTemporaryFile(
            mode="w", suffix=".txt", delete=False
        )
        self.input_file.write("This is a test file.\n")
        self.input_file.close()

        # Creating a temporary output directory
        self.output_dir = tempfile.TemporaryDirectory()

        # Defining the stylesheet, language, and sidebar
        self.stylesheet = "https://cdn.jsdelivr.net/npm/water.css@2/out/water.css"
        self.lang = "en-CA"
        self.sidebar = None

    def tearDown(self):
        # Deleting the temporary input file and output directory
        os.remove(self.input_file.name)
        self.output_dir.cleanup()

 def test_text_to_html_txt_conversion(self):
        with self.assertRaises(SystemExit) as cm:
            # Calling the text_to_html function and store the HTML content
            html_content = text_to_html(
                self.input_file.name,
                self.stylesheet,
                self.output_dir.name,
                self.lang,
                self.sidebar,
            )

            self.assertEqual(cm.exception.code, 0)

            # Defining the expected HTML content
            expected_html_content = (
                f"<!DOCTYPE html>\n"
                f'<html lang="{self.lang}">\n'
                f"\t<head>\n"
                f"\t\t<meta charset='utf-8'>\n"
                f"\t\t<title>This is a test file</title>\n"
                f"\t\t<meta name='viewport' content='width=device-width, initial-scale=1'>\n"
                f'\t\t<link rel="stylesheet" type="text/css" href="{self.stylesheet}">\n'
                f"\t</head>\n"
                f"\t<body>\n"
                f"\t\t<p> This is a test file.</p>\n"
                f"\t</body>\n"
                f"</html>"
            )

            # Check if the HTML content matches the expected HTML content
            self.assertEqual(html_content, expected_html_content)

if __name__ == "__main__":
    unittest.main()
```
You can see that in the `setUp` function I prepare the variables and files that will be used in the `text_to_html` function, and in `tearDown` I make sure to remove the temp file and folder that were made in `setUp`. Also, since I've created my program to exit with a code of 0 when successfully executed I had to wrap the entire test function in `with self.assertRaises(SystemExit) as cm:`. This line can be used to determine when the code exits when an exception is raised or if it exits with a code. In this case, I wanted to make sure that the function exits with a code of 0. So after the function runs, I put this line `self.assertEqual(cm.exception.code, 0) right after the function to make sure it worked properly before comparing the return value with the `expected_html_content` variable.


### What did I learn From Testing and What do I Think of it?
Overall, since I have had no real testing experience, other than looking at testing code my professors would write for my class. I feel that I learned quite a bit from the testers I wrote. I learned how I could make temporary files and folders to work with the function I made. Which I could then use on any functions that handle file or folder input in any future Python applications I make. Additionally, I know how to test the return value of other functions that I make, whether they return a value or a bool. Based on this, I think that I would create testers for any projects I would make in the future, as while it was somewhat time consuming, I still enjoyed it as to me, it added another layer of confidence that the code was working.

Well, that concludes today's post. Thank you all for reading and continuing to follow my journey through programming. I'll catch you in the next post!