Migrating from unittest to pytest
Learn how to migrate unittest test suites to pytest using Codegen
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.
You can find the complete example code in our examples repository.
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.TestCaseinheritance - Converting
setUpandtearDownmethods 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.pyStep 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_pathfor temporary directoriescapsysfor capturing stdout/stderrmonkeypatchfor modifying objectscaplogfor capturing log messages