Migrating from unittest to pytest involves converting test classes and assertions to pytest’s more modern and concise style. This guide will walk you through using Codegen to automate this migration.
Overview
The migration process involves four main steps:
- Converting test class inheritance and setup/teardown methods
- Updating assertions to pytest style
- Converting test discovery patterns
- Modernizing fixture usage
Let’s walk through each step using Codegen.
Step 1: Convert Test Classes and Setup Methods
The first step is to convert unittest’s class-based tests to pytest’s function-based style. This includes:
- Removing
unittest.TestCase
inheritance
- Converting
setUp
and tearDown
methods to fixtures
- Updating class-level setup methods
# From:
class TestUsers(unittest.TestCase):
def setUp(self):
self.db = setup_test_db()
def tearDown(self):
self.db.cleanup()
def test_create_user(self):
user = self.db.create_user("test")
self.assertEqual(user.name, "test")
# To:
import pytest
@pytest.fixture
def db():
db = setup_test_db()
yield db
db.cleanup()
def test_create_user(db):
user = db.create_user("test")
assert user.name == "test"
Step 2: Update Assertions
Next, we’ll convert unittest’s assertion methods to pytest’s plain assert statements:
# From:
def test_user_validation(self):
self.assertTrue(is_valid_email("user@example.com"))
self.assertFalse(is_valid_email("invalid"))
self.assertEqual(get_user_count(), 0)
self.assertIn("admin", get_roles())
self.assertRaises(ValueError, parse_user_id, "invalid")
# To:
def test_user_validation():
assert is_valid_email("user@example.com")
assert not is_valid_email("invalid")
assert get_user_count() == 0
assert "admin" in get_roles()
with pytest.raises(ValueError):
parse_user_id("invalid")
Step 3: Update Test Discovery
pytest uses a different test discovery pattern than unittest. We’ll update the test file names and patterns:
# From:
if __name__ == '__main__':
unittest.main()
# To:
# Remove the unittest.main() block entirely
# Rename test files to test_*.py or *_test.py
Step 4: Modernize Fixture Usage
Finally, we’ll update how test dependencies are managed using pytest’s powerful fixture system:
# From:
class TestDatabase(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.db_conn = create_test_db()
def setUp(self):
self.transaction = self.db_conn.begin()
def tearDown(self):
self.transaction.rollback()
# To:
@pytest.fixture(scope="session")
def db_conn():
return create_test_db()
@pytest.fixture
def transaction(db_conn):
transaction = db_conn.begin()
yield transaction
transaction.rollback()
Common Patterns
Here are some common patterns you’ll encounter when migrating to pytest:
- Parameterized Tests
# From:
def test_validation(self):
test_cases = [("valid@email.com", True), ("invalid", False)]
for email, expected in test_cases:
with self.subTest(email=email):
self.assertEqual(is_valid_email(email), expected)
# To:
@pytest.mark.parametrize("email,expected", [
("valid@email.com", True),
("invalid", False)
])
def test_validation(email, expected):
assert is_valid_email(email) == expected
- Exception Testing
# From:
def test_exceptions(self):
self.assertRaises(ValueError, process_data, None)
with self.assertRaises(TypeError):
process_data(123)
# To:
def test_exceptions():
with pytest.raises(ValueError):
process_data(None)
with pytest.raises(TypeError):
process_data(123)
- Temporary Resources
# From:
def setUp(self):
self.temp_dir = tempfile.mkdtemp()
def tearDown(self):
shutil.rmtree(self.temp_dir)
# To:
@pytest.fixture
def temp_dir():
dir = tempfile.mkdtemp()
yield dir
shutil.rmtree(dir)
Tips and Notes
-
pytest fixtures are more flexible than unittest’s setup/teardown methods:
- They can be shared across test files
- They support different scopes (function, class, module, session)
- They can be parameterized
-
pytest’s assertion introspection provides better error messages by default:
# pytest shows a detailed comparison
assert result == expected
-
You can gradually migrate to pytest:
- pytest can run unittest-style tests
- Convert one test file at a time
- Start with assertion style updates before moving to fixtures
-
Consider using pytest’s built-in fixtures:
tmp_path
for temporary directories
capsys
for capturing stdout/stderr
monkeypatch
for modifying objects
caplog
for capturing log messages