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.


  1. Prepare your test environment
  2. Prepare your test directory
  3. Write your unit test
  4. Run the test
  5. 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:

no_package = True
skip_missing_interpreters = True
min_version = 4.0.0
env_list = unit

src_path = {tox_root}/src
tests_path = {tox_root}/tests

set_env =
    PYTHONPATH = {tox_root}/lib:{[vars]src_path}
pass_env =

description = Run unit tests
deps =
    -r {tox_root}/requirements.txt
commands =
    coverage run --source={[vars]src_path} \
                 -m pytest \
                 --tb native \
                 -v \
                 -s \
                 {posargs} \
    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)

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.

        "charm.FastAPIDemoCharm.version", new_callable=unittest.mock.PropertyMock
    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= --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
        # 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(
        # 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.