← Back to DevBytes

Testing CherryPy Applications: Unit Tests to Integration

Introduction to Testing CherryPy Applications

CherryPy is a minimalist, object-oriented web framework for Python that allows developers to build web applications quickly and cleanly. Like any web framework, applications built with CherryPy benefit enormously from a robust testing strategy. This tutorial walks you through the full spectrum of testing CherryPy applications — from isolated unit tests that verify individual functions, all the way to integration tests that exercise the entire HTTP request/response cycle.

By the end of this guide, you will understand how to structure your test suite, leverage CherryPy's built-in testing utilities, mock external dependencies, and follow industry best practices to keep your application reliable as it grows.

Why Testing Matters

Testing is not just about catching bugs before they reach production — it is about creating a safety net that allows you to refactor, add features, and fix issues with confidence. For CherryPy applications specifically, testing helps you:

Setting Up the Test Environment

Before writing tests, you need to set up your project structure and install the necessary dependencies. The standard library's unittest module is sufficient for most needs, but pytest is a popular alternative that offers a more concise syntax and powerful fixtures.

Project Structure

A well-organized project separates application code from test code. Here is a recommended layout:

my_cherrypy_app/
├── app/
│   ├── __init__.py
│   ├── controllers.py
│   ├── models.py
│   └── services.py
├── tests/
│   ├── __init__.py
│   ├── test_controllers.py
│   ├── test_models.py
│   ├── test_services.py
│   └── test_integration.py
├── requirements.txt
└── run.py

Installing Dependencies

Install CherryPy and your testing tools using pip:

pip install cherrypy pytest pytest-cov

For mocking, Python 3.3+ includes unittest.mock in the standard library, so no additional installation is needed for basic mocking.

A Sample CherryPy Application

To make the examples concrete, let's build a small application that we will test throughout this tutorial. Create app/controllers.py:

import cherrypy
import json
from app.services import UserService

class Root(object):

    def __init__(self):
        self.user_service = UserService()

    @cherrypy.expose
    def index(self):
        return "Welcome to the CherryPy Test Demo!"

    @cherrypy.expose
    def users(self, user_id=None):
        if user_id is None:
            users = self.user_service.get_all_users()
            return json.dumps(users)
        else:
            user = self.user_service.get_user(int(user_id))
            if user is None:
                raise cherrypy.HTTPError(404, "User not found")
            return json.dumps(user)

    @cherrypy.expose
    @cherrypy.tools.allow(methods=['POST'])
    def create_user(self, name, email):
        user = self.user_service.create_user(name, email)
        cherrypy.response.status = 201
        return json.dumps(user)

Now create app/services.py with the business logic separated from the controller:

class UserService(object):

    def __init__(self):
        self._users = {}
        self._next_id = 1

    def get_all_users(self):
        return list(self._users.values())

    def get_user(self, user_id):
        return self._users.get(user_id)

    def create_user(self, name, email):
        if not name or not email:
            raise ValueError("Name and email are required")
        if "@" not in email:
            raise ValueError("Invalid email format")
        user = {
            "id": self._next_id,
            "name": name,
            "email": email
        }
        self._users[self._next_id] = user
        self._next_id += 1
        return user

Finally, create run.py to start the application:

import cherrypy
from app.controllers import Root

if __name__ == '__main__':
    cherrypy.quickstart(Root(), '/')

Unit Testing CherryPy Components

Unit tests focus on testing individual components in isolation. In a CherryPy application, the most important components to unit test are your service classes and any utility functions. Because these components do not depend on the CherryPy framework itself, they are straightforward to test.

Testing the Service Layer

Create tests/test_services.py to test the UserService class:

import unittest
from app.services import UserService

class TestUserService(unittest.TestCase):

    def setUp(self):
        self.service = UserService()

    def test_create_user_success(self):
        user = self.service.create_user("Alice", "alice@example.com")
        self.assertEqual(user["name"], "Alice")
        self.assertEqual(user["email"], "alice@example.com")
        self.assertEqual(user["id"], 1)

    def test_create_user_increments_id(self):
        user1 = self.service.create_user("Alice", "alice@example.com")
        user2 = self.service.create_user("Bob", "bob@example.com")
        self.assertEqual(user2["id"], user1["id"] + 1)

    def test_create_user_missing_name_raises_error(self):
        with self.assertRaises(ValueError) as context:
            self.service.create_user("", "alice@example.com")
        self.assertIn("Name and email are required", str(context.exception))

    def test_create_user_invalid_email_raises_error(self):
        with self.assertRaises(ValueError) as context:
            self.service.create_user("Alice", "not-an-email")
        self.assertIn("Invalid email format", str(context.exception))

    def test_get_user_returns_none_for_missing_id(self):
        result = self.service.get_user(999)
        self.assertIsNone(result)

    def test_get_all_users_returns_list(self):
        self.service.create_user("Alice", "alice@example.com")
        self.service.create_user("Bob", "bob@example.com")
        users = self.service.get_all_users()
        self.assertEqual(len(users), 2)

    def test_get_all_users_empty_returns_empty_list(self):
        users = self.service.get_all_users()
        self.assertEqual(users, [])

if __name__ == '__main__':
    unittest.main()

Run these tests with:

python -m pytest tests/test_services.py -v

These tests are pure unit tests because they do not interact with CherryPy at all. They simply verify that the business logic in UserService behaves correctly. This is the ideal starting point for your test suite.

Testing Controllers with CherryPy's Built-in Test Tools

CherryPy provides a built-in testing module called cherrypy.test.helper that allows you to start a CherryPy server in-process and make real HTTP requests against it. This is useful for testing your controllers and routing logic without needing an external HTTP client or a running server.

Using CherryPy's Test Helper

Create tests/test_controllers.py:

import unittest
import json
from unittest.mock import MagicMock, patch
import cherrypy
from cherrypy.test import helper
from app.controllers import Root

class TestRootController(helper.CPWebCase):

    @staticmethod
    def setup_server():
        cherrypy.tree.mount(Root(), '/')
        cherrypy.config.update({
            'environment': 'production',
            'log.screen': False,
        })

    def test_index_returns_welcome_message(self):
        self.getPage("/")
        self.assertStatus("200 OK")
        self.assertInBody("Welcome to the CherryPy Test Demo!")

    def test_users_list_returns_json(self):
        self.getPage("/users")
        self.assertStatus("200 OK")
        body = self.body.decode('utf-8')
        data = json.loads(body)
        self.assertIsInstance(data, list)

    def test_get_user_by_id_returns_user(self):
        # First create a user
        self.getPage("/create_user",
                     method="POST",
                     headers=[("Content-Type", "application/x-www-form-urlencoded")],
                     body="name=Alice&email=alice@example.com")
        self.assertStatus("201 Created")

        # Then retrieve it
        self.getPage("/users?user_id=1")
        self.assertStatus("200 OK")
        body = self.body.decode('utf-8')
        data = json.loads(body)
        self.assertEqual(data["name"], "Alice")
        self.assertEqual(data["email"], "alice@example.com")

    def test_get_nonexistent_user_returns_404(self):
        self.getPage("/users?user_id=999")
        self.assertStatus(404)

    def test_create_user_returns_201(self):
        self.getPage("/create_user",
                     method="POST",
                     headers=[("Content-Type", "application/x-www-form-urlencoded")],
                     body="name=Bob&email=bob@example.com")
        self.assertStatus("201 Created")
        body = self.body.decode('utf-8')
        data = json.loads(body)
        self.assertEqual(data["name"], "Bob")

if __name__ == '__main__':
    unittest.main()

The helper.CPWebCase class provides methods like getPage() to simulate HTTP requests and assertion methods like assertStatus() and assertInBody() to verify responses. The setup_server() method is called once before the test class runs and configures the CherryPy application in a test environment.

Key Methods of CPWebCase

Mocking and Patching Dependencies

When testing controllers, you often want to isolate them from their dependencies — such as databases, external APIs, or service classes — so that you are only testing the controller's behavior. Python's unittest.mock module makes this straightforward.

Mocking the Service Layer

Here is an example of testing the Root controller with a mocked UserService:

import unittest
import json
from unittest.mock import MagicMock, patch
import cherrypy
from cherrypy.test import helper
from app.controllers import Root

class TestRootControllerWithMocks(helper.CPWebCase):

    @staticmethod
    def setup_server():
        root = Root()
        # Replace the real service with a mock
        root.user_service = MagicMock()
        root.user_service.get_all_users.return_value = [
            {"id": 1, "name": "Alice", "email": "alice@example.com"},
            {"id": 2, "name": "Bob", "email": "bob@example.com"}
        ]
        root.user_service.get_user.return_value = {
            "id": 1, "name": "Alice", "email": "alice@example.com"
        }
        root.user_service.create_user.return_value = {
            "id": 3, "name": "Charlie", "email": "charlie@example.com"
        }
        cherrypy.tree.mount(root, '/')
        cherrypy.config.update({
            'environment': 'production',
            'log.screen': False,
        })

    def test_users_list_with_mocked_service(self):
        self.getPage("/users")
        self.assertStatus("200 OK")
        body = self.body.decode('utf-8')
        data = json.loads(body)
        self.assertEqual(len(data), 2)
        self.assertEqual(data[0]["name"], "Alice")

    def test_get_user_with_mocked_service(self):
        self.getPage("/users?user_id=1")
        self.assertStatus("200 OK")
        body = self.body.decode('utf-8')
        data = json.loads(body)
        self.assertEqual(data["name"], "Alice")

    def test_create_user_with_mocked_service(self):
        self.getPage("/create_user",
                     method="POST",
                     headers=[("Content-Type", "application/x-www-form-urlencoded")],
                     body="name=Charlie&email=charlie@example.com")
        self.assertStatus("201 Created")
        body = self.body.decode('utf-8')
        data = json.loads(body)
        self.assertEqual(data["name"], "Charlie")
        self.assertEqual(data["id"], 3)

if __name__ == '__main__':
    unittest.main()

By replacing the real UserService with a MagicMock, you ensure that the controller tests do not depend on the service's implementation. This means if the service has a bug, your controller tests will still pass, and the service tests will catch the issue. This separation is a key principle of effective unit testing.

Patching at the Module Level

Sometimes you need to patch a dependency that is imported at the module level. You can use the patch decorator or context manager for this:

import unittest
from unittest.mock import patch, MagicMock
from app.services import UserService

class TestUserServiceWithPatch(unittest.TestCase):

    @patch('app.services.UserService.create_user')
    def test_create_user_is_called_with_correct_args(self, mock_create):
        mock_create.return_value = {"id": 1, "name": "Alice", "email": "alice@example.com"}
        service = UserService()
        result = service.create_user("Alice", "alice@example.com")
        mock_create.assert_called_once_with("Alice", "alice@example.com")
        self.assertEqual(result["name"], "Alice")

    def test_get_user_with_patched_storage(self):
        service = UserService()
        with patch.object(service, '_users', {1: {"id": 1, "name": "Alice", "email": "alice@example.com"}}):
            user = service.get_user(1)
            self.assertEqual(user["name"], "Alice")

if __name__ == '__main__':
    unittest.main()

Integration Testing

Integration tests verify that multiple components work together correctly. In a CherryPy application, this typically means testing the full request/response cycle — from the HTTP request entering the application, through routing, controller logic, service layer, and back to the HTTP response.

Full-Stack Integration Tests

Create tests/test_integration.py to test the entire application end-to-end:

import unittest
import json
import cherrypy
from cherrypy.test import helper
from app.controllers import Root
from app.services import UserService

class TestApplicationIntegration(helper.CPWebCase):

    @staticmethod
    def setup_server():
        root = Root()
        # Use a real UserService instance for integration testing
        root.user_service = UserService()
        cherrypy.tree.mount(root, '/', {
            '/': {
                'tools.sessions.on': True,
                'tools.encode.on': True,
                'tools.encode.encoding': 'utf-8',
            }
        })
        cherrypy.config.update({
            'environment': 'production',
            'log.screen': False,
        })

    def test_full_user_lifecycle(self):
        """Test creating, retrieving, and listing users in sequence."""
        # Step 1: Verify the app starts with no users
        self.getPage("/users")
        self.assertStatus("200 OK")
        data = json.loads(self.body.decode('utf-8'))
        self.assertEqual(data, [])

        # Step 2: Create a user
        self.getPage("/create_user",
                     method="POST",
                     headers=[("Content-Type", "application/x-www-form-urlencoded")],
                     body="name=Alice&email=alice@example.com")
        self.assertStatus("201 Created")
        user1 = json.loads(self.body.decode('utf-8'))
        self.assertEqual(user1["id"], 1)

        # Step 3: Create a second user
        self.getPage("/create_user",
                     method="POST",
                     headers=[("Content-Type", "application/x-www-form-urlencoded")],
                     body="name=Bob&email=bob@example.com")
        self.assertStatus("201 Created")
        user2 = json.loads(self.body.decode('utf-8'))
        self.assertEqual(user2["id"], 2)

        # Step 4: List all users
        self.getPage("/users")
        self.assertStatus("200 OK")
        users = json.loads(self.body.decode('utf-8'))
        self.assertEqual(len(users), 2)

        # Step 5: Retrieve a specific user
        self.getPage("/users?user_id=1")
        self.assertStatus("200 OK")
        retrieved = json.loads(self.body.decode('utf-8'))
        self.assertEqual(retrieved["name"], "Alice")

        # Step 6: Try to retrieve a nonexistent user
        self.getPage("/users?user_id=999")
        self.assertStatus(404)

    def test_index_page_loads(self):
        """Verify the index page is accessible."""
        self.getPage("/")
        self.assertStatus("200 OK")
        self.assertInBody("Welcome")

    def test_invalid_post_method_rejected(self):
        """Verify that GET requests to POST-only endpoints are rejected."""
        self.getPage("/create_user")
        # CherryPy's allow tool returns 405 Method Not Allowed
        self.assertStatus(405)

if __name__ == '__main__':
    unittest.main()

Integration tests like these are valuable because they catch issues that unit tests might miss — such as routing misconfigurations, middleware problems, or serialization errors. They give you confidence that the entire application works as expected when all the pieces are connected.

Testing with a Real Database

If your application uses a database, integration tests should ideally run against a test database rather than your production database. Here is an example pattern for managing a test database:

import unittest
import os
import sqlite3
import cherrypy
from cherrypy.test import helper
from app.controllers import Root
from app.services import UserService

class TestWithDatabase(helper.CPWebCase):

    DB_PATH = "test_app.db"

    @classmethod
    def setUpClass(cls):
        # Create a fresh test database
        if os.path.exists(cls.DB_PATH):
            os.remove(cls.DB_PATH)
        conn = sqlite3.connect(cls.DB_PATH)
        conn.execute("""
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                email TEXT NOT NULL
            )
        """)
        conn.commit()
        conn.close()

    @classmethod
    def tearDownClass(cls):
        if os.path.exists(cls.DB_PATH):
            os.remove(cls.DB_PATH)

    @staticmethod
    def setup_server():
        root = Root()
        root.user_service = UserService(db_path=TestWithDatabase.DB_PATH)
        cherrypy.tree.mount(root, '/')
        cherrypy.config.update({
            'environment': 'production',
            'log.screen': False,
        })

    def test_user_persists_across_requests(self):
        # Create a user
        self.getPage("/create_user",
                     method="POST",
                     headers=[("Content-Type", "application/x-www-form-urlencoded")],
                     body="name=Alice&email=alice@example.com")
        self.assertStatus("201 Created")

        # Retrieve it in a separate request
        self.getPage("/users?user_id=1")
        self.assertStatus("200 OK")
        data = json.loads(self.body.decode('utf-8'))
        self.assertEqual(data["name"], "Alice")

if __name__ == '__main__':
    unittest.main()

Using pytest with CherryPy

While CherryPy's built-in test helper is based on unittest, you can also use pytest to run your tests. Pytest can discover and run unittest.TestCase classes out of the box. Additionally, you can write pytest-style tests for your service layer:

import pytest
from app.services import UserService

@pytest.fixture
def user_service():
    return UserService()

def test_create_user(user_service):
    user = user_service.create_user("Alice", "alice@example.com")
    assert user["name"] == "Alice"
    assert user["email"] == "alice@example.com"
    assert user["id"] == 1

def test_create_multiple_users(user_service):
    user_service.create_user("Alice", "alice@example.com")
    user_service.create_user("Bob", "bob@example.com")
    users = user_service.get_all_users()
    assert len(users) == 2

def test_invalid_email_raises_error(user_service):
    with pytest.raises(ValueError, match="Invalid email format"):
        user_service.create_user("Alice", "not-an-email")

def test_missing_name_raises_error(user_service):
    with pytest.raises(ValueError, match="Name and email are required"):
        user_service.create_user("", "alice@example.com")

def test_get_nonexistent_user_returns_none(user_service):
    assert user_service.get_user(999) is None

Run all tests with pytest:

python -m pytest tests/ -v --cov=app

The --cov=app flag generates a coverage report showing which lines of your application code are exercised by your tests.

Best Practices for Testing CherryPy Applications

1. Separate Business Logic from Controllers

Keep your CherryPy handler methods thin. Move business logic into service classes that can be unit tested independently. This makes your tests faster, more focused, and easier to maintain.

2. Test at Multiple Levels

A healthy test suite includes tests at multiple levels of granularity:

3. Use Fixtures for Common Setup

Whether you use unittest's setUp method or pytest fixtures, avoid duplicating setup code across tests. Centralize common initialization logic so your tests remain clean and maintainable.

4. Mock External Dependencies

External APIs, databases, and file systems should be mocked in unit tests to keep tests fast and deterministic. Reserve real database connections and network calls for integration tests.

5. Test Edge Cases and Error Conditions

Do not only test the happy path. Write tests for invalid inputs, missing parameters, unauthorized access, and server errors. CherryPy's HTTPError exceptions should be tested to ensure they produce the correct status codes.

6. Keep Tests Independent

Each test should be able to run in isolation without depending on the state created by another test. Use fresh instances of services and reset state in setUp or fixtures. This prevents flaky tests and makes debugging easier.

7. Aim for Meaningful Coverage

Coverage is a useful metric, but 100% coverage does not guarantee quality. Focus on testing the most critical and complex parts of your application. Use coverage reports to identify untested code paths, but do not chase a number at the expense of test quality.

8. Run Tests in CI

Integrate your test suite into a continuous integration pipeline. Run tests on every commit and pull request to catch regressions early. Tools like GitHub Actions, GitLab CI, and Jenkins all support Python test execution out of the box.

Conclusion

Testing CherryPy applications effectively requires a layered approach: unit tests for your business logic, controller tests using CherryPy's built-in test helper for routing and request handling, and integration tests for the full request/response cycle. By separating concerns, mocking external dependencies, and following best practices like testing edge cases and keeping tests independent, you can build a robust test suite that gives you confidence in your application's reliability. CherryPy's simplicity and object-oriented design make it particularly well-suited to testable code — take advantage of this by investing in a comprehensive testing strategy from the start. Your future self, and your users, will thank you.

— Ad —

Google AdSense will appear here after approval

← Back to all articles