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 Graph-sitter 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