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:
- Verify routing logic: Ensure that URLs map to the correct handler methods and that parameters are parsed correctly.
- Validate response behavior: Confirm that your handlers return the expected status codes, headers, and body content.
- Catch regressions early: When you change one part of your application, tests immediately tell you if you broke something elsewhere.
- Document expected behavior: Well-written tests serve as living documentation for how your application should behave.
- Improve design: Writing testable code often leads to better separation of concerns and cleaner architecture.
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
getPage(url, method, headers, body)— Makes an HTTP request to the mounted application.assertStatus(expected)— Asserts the HTTP response status code.assertInBody(text)— Asserts that the given text appears in the response body.assertNotInBody(text)— Asserts that the given text does not appear in the response body.assertHeader(name, value)— Asserts that a specific response header exists.
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:
- Unit tests for individual functions and service methods — fast and isolated.
- Controller tests for routing and request handling — use CherryPy's test helper or mocks.
- Integration tests for the full request/response cycle — slower but comprehensive.
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.