Writing Tests For Your Python Project

I began this little trip with the post Exporting CloudWatch Logs to S3 that provided example code to get logs from Cloud Watch to S3. From there, the code got prettier with Adding pre-commit Hooks to Python Repo. The next logical step is to make sure the code functions exactly like we’d expect. This can be done by writing tests to make sure our code functions like we’d expect. I’ve highlighted a number of reasons why you should create tests in the article Top 5 Reasons to Build Tests for Your Code.

Setting Up Our Environment

Setup is pretty simple as we only need to install pytest. This can be installed using pip:

pip install pytest

Next, we’ll just start by creating a very simple file named the same as our module to test prepended with test_. The previous posts had a file called cloudwatch_to_s3.py so we simple create a test_cloudwatch_to_s3.py in the same directory. Pytest will look for any files that start with test_ and attempt to execute tests contained within those files.

The initial contents of this file will be pretty simple:

import logging
import cloudwatch_to_s3
import pytest

class TestCloudWatch:
  LOGGER = logging.getLogger(__name__)

We’re bringing in logging in case we want to log any debug or errors. We also need pytest in order to handle certain test results. The module we’re testing is also imported. I’m building this test as a class so that I can build all of my tests as functions and handle any other mocking and setup I might need for testing.

Creating The First Test

Adding tests to our test file is pretty simple. I typically go through my code and just identify each function from the top to the bottom. Each function should have it’s variations tested. For instance, the cloudwatch_to_s3.py file starts with the following function:

def generate_date_dict(n_days=1):
    currentTime = datetime.datetime.now()
    date_dict = {
        "start_date": currentTime - datetime.timedelta(days=n_days),
        "end_date": currentTime - datetime.timedelta(days=n_days - 1),
    }
    return date_dict

In building a test for this function, I have the following:

import logging
import cloudwatch_to_s3
import pytest
import datetime

class TestCloudWatch:
  LOGGER = logging.getLogger(__name__)

  def test_generate_date_dict(self):
    currentTime = datetime.datetime.now()
    start_date = currentTime - datetime.timedelta(days=1)
    end_date = currentTime - datetime.timedelta(days=1 - 1)
    result = cloudwatch_to_s3.generate_date_dict(1)
    assert start_date.strftime("%m/%d/%Y, %H:%M") == result["start_date"].strftime("%m/%d/%Y, %H:%M") and end_date.strftime("%m/%d/%Y, %H:%M") == result["end_date"].strftime("%m/%d/%Y, %H:%M")

In order to test this function, we need to add datetime because we have to generate a date to compare against our testing. When creating each function within the class, we must make sure they start with test. I try to name my tests with test_<whatever_function_im_testing>. This isn’t the most ideal test since we need to run the results through strftime to get rid of the seconds since the times will never be identical due to them being created at separate points. We could try to mock things but let’s save that for later. The key word in our test here is assert. We have to use this make sure our test is True. If the test is False for some reason, we will fail. We can now run the pytest command to see what happens:

root@40f85188bc2a:/opt/app/modules# pytest
==================================================================================== test session starts =====================================================================================
platform linux -- Python 3.10.6, pytest-7.2.0, pluggy-1.0.0
rootdir: /opt/app/modules
collected 1 item                                                                                                                                                                             

test_cloudwatch_to_s3.py .                                                                                                                                                             [100%]

===================================================================================== 1 passed in 0.11s ======================================================================================

Good news! We have a single test that has passed!

Code Test Coverage Check

Yes, we know we have very little code coverage at this point but let’s assume we’ve written a few tests and we want to see how good our tests are. First, we now need to pip install pytest-cov and then we’ll update the arguments to our pytest command to include --cov=modules --cov=classes --cov-report term-missing:

root@40f85188bc2a:/opt/app# pytest --cov=modules --cov=classes --cov-report term-missing
==================================================================================== test session starts =====================================================================================
platform linux -- Python 3.10.6, pytest-7.2.0, pluggy-1.0.0
rootdir: /opt/app
plugins: cov-4.0.0
collected 1 item                                                                                                                                                                             

modules/test_cloudwatch_to_s3.py .                                                                                                                                                     [100%]

---------- coverage: platform linux, python 3.10.6-final-0 -----------
Name                               Stmts   Miss  Cover   Missing
----------------------------------------------------------------
modules/cloudwatch_to_s3.py           83     62    25%   33, 43, 57-73, 83-115, 119-140, 149-174
modules/test_cloudwatch_to_s3.py      12      0   100%
----------------------------------------------------------------
TOTAL                                 95     62    35%


===================================================================================== 1 passed in 0.38s ======================================================================================

In addition to getting a passed status, we also see that we are testing 25% of our module and we can see the specific lines that we’re missing.

Adding a Few More Tests

The next functions in the code are convert_from_date and generate_prefix so we’ll add some tests for them quickly:

import logging
import cloudwatch_to_s3
import pytest
import datetime
import os

class TestCloudWatch:
  LOGGER = logging.getLogger(__name__)

  def test_generate_date_dict(self):
    currentTime = datetime.datetime.now()
    start_date = currentTime - datetime.timedelta(days=1)
    end_date = currentTime - datetime.timedelta(days=1 - 1)
    result = cloudwatch_to_s3.generate_date_dict(1)
    assert start_date.strftime("%m/%d/%Y, %H:%M") == result["start_date"].strftime("%m/%d/%Y, %H:%M") and end_date.strftime("%m/%d/%Y, %H:%M") == result["end_date"].strftime("%m/%d/%Y, %H:%M")

  def test_convert_from_date(self):
    currentTime = datetime.datetime.now()
    expected = int(currentTime.timestamp() * 1000)
    result = cloudwatch_to_s3.convert_from_date(currentTime)
    assert result == expected

  def test_generate_prefix(self):
    currentTime = datetime.datetime.now()
    expected = os.path.join("test_name", currentTime.strftime("%Y{0}%m{0}%d").format(os.path.sep))
    result = cloudwatch_to_s3.generate_prefix("test_name", currentTime)
    assert result == expected

I’m using os in my case so this has been added to the import list. With these two new tests added, let’s run pytest with our coverage report.

root@40f85188bc2a:/opt/app# pytest --cov=modules --cov=classes --cov-report term-missing
==================================================================================== test session starts =====================================================================================
platform linux -- Python 3.10.6, pytest-7.2.0, pluggy-1.0.0
rootdir: /opt/app
plugins: cov-4.0.0
collected 3 items                                                                                                                                                                            

modules/test_cloudwatch_to_s3.py ...                                                                                                                                                   [100%]


---------- coverage: platform linux, python 3.10.6-final-0 -----------
Name                               Stmts   Miss  Cover   Missing
----------------------------------------------------------------
modules/cloudwatch_to_s3.py           83     60    28%   57-73, 83-115, 119-140, 149-174
modules/test_cloudwatch_to_s3.py      23      0   100%
----------------------------------------------------------------
TOTAL                                106     60    43%


===================================================================================== 3 passed in 0.39s ======================================================================================

Look at the results! We now have 3 passing tests and we’re now testing 28% of our code! Baby steps!

Conclusion

At this point, we’ve started creating test cases. The functions that remain are now those that require interaction with AWS as well as some other mocking. This is a good time to take a break from the console and look for the next article to finish our tests and explore mocking. Stay Tuned!