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 <a href="https://docs.pytest.org/en/7.2.x/" rel="noreferrer noopener" target="_blank">pytest</a>
. 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!