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.

You can find the complete example code in our examples repository.

Overview

The migration process involves four main steps:

  1. Converting test class inheritance and setup/teardown methods
  2. Updating assertions to pytest style
  3. Converting test discovery patterns
  4. 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:

  1. 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
  1. 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)
  1. 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

  1. 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
  2. pytest’s assertion introspection provides better error messages by default:

    # pytest shows a detailed comparison
    assert result == expected
    
  3. 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
  4. 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