Write unit tests for your charm
From Zero to Hero: Write your first Kubernetes charm > Write a unit test for your charm
See previous: Observe your charm with COS Lite
This document is part of a series, and we recommend you follow it in sequence. However, you can also jump straight in by checking out the code from the previous branches:
git clone https://github.com/canonical/juju-sdk-tutorial-k8s.git
cd juju-sdk-tutorial-k8s
git checkout 07_cos_integration
git checkout -b 08_unit_testing
When you’re writing a charm, you will want to ensure that it will behave reliably as intended.
For example, that the various components – relation data, pebble services, or configuration files – all behave as expected in response to an event.
You can ensure all this by writing a rich battery of units tests. In the context of a charm this is usually done using pytest
and/or unittest
and especially the operator framework’s built-in testing library – ops.testing.Harness
.
In this chapter you will write a simple unit test to check that your workload container is initialised correctly.
Contents:
- Prepare your test environment
- Prepare your test directory
- Write your unit test
- Run the test
- Review the final code
Prepare your test environment
In the ~/tox.ini
file of your charm project’s root directory, add the following to configure your test environment:
[tox]
no_package = True
skip_missing_interpreters = True
min_version = 4.0.0
env_list = unit
[vars]
src_path = {tox_root}/src
tests_path = {tox_root}/tests
[testenv]
set_env =
PYTHONPATH = {tox_root}/lib:{[vars]src_path}
PYTHONBREAKPOINT=pdb.set_trace
PY_COLORS=1
pass_env =
PYTHONPATH
CHARM_BUILD_DIR
MODEL_SETTINGS
[testenv:unit]
description = Run unit tests
deps =
pytest
cosl
coverage[toml]
-r {tox_root}/requirements.txt
commands =
coverage run --source={[vars]src_path} \
-m pytest \
--tb native \
-v \
-s \
{posargs} \
{[vars]tests_path}/unit
coverage report
Prepare your test directory
In your project root, create a tests/unit
directory:
mkdir -p tests/unit
Write your unit test
In your tests/unit
directory, create a file called test_charm.py
.
In this file, do all of the following:
First, add the necessary imports:
from charm import FastAPIDemoCharm
import ops
import ops.testing
import unittest
import unittest.mock
Then, add a test class that sets up the testing harness:
class TestCharm(unittest.TestCase):
def setUp(self):
self.harness = ops.testing.Harness(FastAPIDemoCharm)
self.addCleanup(self.harness.cleanup)
self.harness.begin()
Finally, add a first test case as a method of the test class, as below. As you can see, this test case is used to verify that the deployment of the fastapi-service
within the demo-server
container is configured correctly and that the service is started and running as expected when the container is marked as pebble-ready
. It also checks that the unit’s status is set to active without any error messages. Note that we mock some methods of the charm because they do external calls that are not represented in the state of this unit test.
@unittest.mock.patch(
"charm.FastAPIDemoCharm.version", new_callable=unittest.mock.PropertyMock
)
@unittest.mock.patch("charm.FastAPIDemoCharm.fetch_postgres_relation_data")
def test_pebble_layer(self, mock_fetch_postgres_relation_data, mock_version):
# Expected plan after Pebble ready with default config
expected_plan = {
"services": {
"fastapi-service": {
"override": "replace",
"summary": "fastapi demo",
"command": "uvicorn api_demo_server.app:app --host=0.0.0.0 --port=8000",
"startup": "enabled",
"environment": {
"DEMO_SERVER_DB_HOST": None,
"DEMO_SERVER_DB_PASSWORD": None,
"DEMO_SERVER_DB_PORT": None,
"DEMO_SERVER_DB_USER": None,
},
}
}
}
mock_fetch_postgres_relation_data.return_value = {}
mock_version.return_value = "1.0.0"
# Simulate the container coming up and emission of pebble-ready event
self.harness.container_pebble_ready("demo-server")
# Get the plan now we've run PebbleReady
updated_plan = self.harness.get_container_pebble_plan("demo-server").to_dict()
# Check we've got the plan we expected
self.assertEqual(expected_plan, updated_plan)
# Check the service was started
service = self.harness.model.unit.get_container("demo-server").get_service(
"fastapi-service"
)
self.assertTrue(service.is_running())
# Ensure we set an ActiveStatus with no message
self.assertEqual(self.harness.model.unit.status, ops.ActiveStatus())
Read more: Harness
Run the test
In your Multipass Ubuntu VM shell, run your unit test as below:
ubuntu@charm-dev:~/fastapi-demo$ tox -e unit
You should get an output similar to the one below:
unit: commands[0]> coverage run --source=/home/ubuntu/fastapi-demo/src -m pytest --tb native -v -s /home/ubuntu/fastapi-demo/tests/unit
=============================================================================================================================================================================== test session starts ===============================================================================================================================================================================
platform linux -- Python 3.10.12, pytest-7.4.2, pluggy-1.3.0 -- /home/ubuntu/fastapi-demo/.tox/unit/bin/python
cachedir: .tox/unit/.pytest_cache
rootdir: /home/ubuntu/fastapi-demo
collected 1 item
tests/unit/test_charm.py::TestCharm::test_pebble_layer PASSED
================================================================================================================================================================================ 1 passed in 0.30s ================================================================================================================================================================================
unit: commands[1]> coverage report
Name Stmts Miss Cover
----------------------------------
src/charm.py 118 49 58%
----------------------------------
TOTAL 118 49 58%
unit: OK (0.99=setup[0.04]+cmd[0.78,0.16] seconds)
congratulations :) (1.02 seconds)
Congratulations, you have now successfully implemented your first unit test!
As you can see in the output, the current tests cover 58% of the charm code. In a real-life scenario make sure to cover much more!
Review the final code
For the full code see: 08_unit_testing
For a comparative view of the code before and after this doc see: Comparison
See next: Write scenario tests for your charm
Contributors: @bschimke95
Last updated a month ago.