DEV Community

Marco Pasqua
Marco Pasqua

Posted on

Testing Python Code Using UnitTest

Hey everyone, I recently had to create testers for my 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 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, 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.

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()
Enter fullscreen mode Exit fullscreen mode

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;

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>")
Enter fullscreen mode Exit fullscreen mode

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, and formatter, Black, 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. 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 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 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 and 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;

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()
Enter fullscreen mode Exit fullscreen mode

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 theexpected_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!

Top comments (0)