Skip to content

Test Classes

Test classes allow you to group related tests together and share fixtures across test methods. Rustest supports pytest-style test classes.

Basic Test Classes

Create a test class by naming it with a Test prefix:

class TestMathOperations:
    """Group related math tests together."""

    def test_addition(self):
        assert 1 + 1 == 2

    def test_subtraction(self):
        assert 5 - 3 == 2

    def test_multiplication(self):
        assert 3 * 4 == 12

No init Required

Test classes don't need an __init__ method. Rustest creates fresh instances automatically.

Using Fixtures in Test Classes

Test methods can use fixtures just like standalone test functions:

from rustest import fixture

@fixture
def calculator():
    return {"add": lambda x, y: x + y, "multiply": lambda x, y: x * y}

class TestCalculator:
    def test_addition(self, calculator):
        assert calculator["add"](2, 3) == 5

    def test_multiplication(self, calculator):
        assert calculator["multiply"](4, 5) == 20

Class-Scoped Fixtures

Use class-scoped fixtures to share expensive setup across all tests in a class:

from rustest import fixture

@fixture(scope="class")
def database():
    """Shared database connection for all tests in a class."""
    db = {"connection": "db://test", "data": []}
    return db

class TestDatabase:
    def test_connection(self, database):
        assert database["connection"] == "db://test"

    def test_add_data(self, database):
        database["data"].append("item1")
        assert len(database["data"]) == 1

    def test_data_persists(self, database):
        # Same database instance from previous test
        assert len(database["data"]) == 1

Shared State

Class-scoped fixtures maintain state across tests in the class. Be careful with mutable data!

Fixture Methods Within Classes

Define fixtures as methods inside the test class:

from rustest import fixture

class TestUserService:
    @fixture(scope="class")
    def service(self):
        """Class-level fixture shared across all tests."""
        svc = UserService()
        yield svc
        svc.cleanup()

    @fixture
    def user(self, service):
        """Per-test fixture that depends on class fixture."""
        return service.create_user("test_user")

    def test_user_creation(self, user):
        assert user.name == "test_user"

    def test_user_count(self, service, user):
        assert service.count() >= 1

Class and Instance Variables

Class Variables

Class variables are shared across all test methods:

class TestSharedData:
    shared_config = {"debug": True, "timeout": 30}

    def test_config_debug(self):
        assert self.shared_config["debug"] is True

    def test_config_timeout(self):
        assert self.shared_config["timeout"] == 30

Instance Variables

Each test method gets a fresh instance, so instance variables are isolated:

class TestInstanceVariables:
    def test_instance_var_1(self):
        self.value = 10
        assert self.value == 10

    def test_instance_var_2(self):
        # Fresh instance - self.value doesn't exist yet
        self.value = 20
        assert self.value == 20

Parametrized Test Methods

Use @parametrize on class methods:

from rustest import parametrize

class TestStringOperations:
    @parametrize("text,expected", [
        ("hello", "HELLO"),
        ("world", "WORLD"),
        ("Python", "PYTHON"),
    ])
    def test_uppercase(self, text, expected):
        assert text.upper() == expected

    @parametrize("value", [1, 2, 3, 4, 5])
    def test_positive(self, value):
        assert value > 0

Marks on Test Classes

Apply marks to all tests in a class:

from rustest import mark

@mark.integration
class TestDatabaseIntegration:
    """All tests in this class are integration tests."""

    def test_insert(self):
        pass

    def test_update(self):
        pass

    @mark.slow
    def test_bulk_import(self):
        # Has both @mark.integration and @mark.slow
        pass

Organizing Tests with Classes

By Feature

class TestUserAuthentication:
    def test_login_success(self):
        pass

    def test_login_failure(self):
        pass

    def test_logout(self):
        pass

class TestUserProfile:
    def test_update_email(self):
        pass

    def test_update_password(self):
        pass

    def test_delete_account(self):
        pass

By Test Type

from rustest import mark

@mark.unit
class TestUnitMath:
    def test_addition(self):
        assert 1 + 1 == 2

    def test_subtraction(self):
        assert 5 - 3 == 2

@mark.integration
class TestIntegrationAPI:
    def test_get_user(self):
        pass

    def test_create_user(self):
        pass

Nested Test Classes

While rustest supports nested classes, it's generally better to use flat structures:

# Supported but not recommended
class TestOuter:
    class TestInner:
        def test_something(self):
            pass

# Better - use flat structure with descriptive names
class TestOuterInner:
    def test_something(self):
        pass

Real-World Examples

API Testing

from rustest import fixture, mark

@fixture(scope="class")
def api_client():
    client = APIClient("https://api.example.com")
    yield client
    client.close()

@mark.integration
class TestUserAPI:
    def test_get_user(self, api_client):
        response = api_client.get("/users/1")
        assert response.status == 200

    def test_create_user(self, api_client):
        data = {"name": "Alice", "email": "alice@example.com"}
        response = api_client.post("/users", json=data)
        assert response.status == 201

    def test_update_user(self, api_client):
        data = {"email": "newemail@example.com"}
        response = api_client.put("/users/1", json=data)
        assert response.status == 200

Database Testing

from rustest import fixture

@fixture(scope="class")
def db_connection():
    conn = connect_to_database()
    setup_test_schema(conn)
    yield conn
    teardown_test_schema(conn)
    conn.close()

class TestUserRepository:
    @fixture
    def repository(self, db_connection):
        return UserRepository(db_connection)

    def test_create_user(self, repository):
        user = repository.create("Alice")
        assert user.name == "Alice"

    def test_find_user(self, repository):
        user = repository.find_by_name("Alice")
        assert user is not None

    def test_delete_user(self, repository):
        repository.delete("Alice")
        user = repository.find_by_name("Alice")
        assert user is None

Service Testing

from rustest import fixture, parametrize

class TestEmailService:
    @fixture(scope="class")
    def email_service(self):
        service = EmailService()
        service.connect()
        yield service
        service.disconnect()

    @parametrize("email,valid", [
        ("user@example.com", True),
        ("invalid-email", False),
        ("@example.com", False),
        ("user@", False),
    ])
    def test_email_validation(self, email_service, email, valid):
        result = email_service.validate(email)
        assert result == valid

    def test_send_email(self, email_service):
        result = email_service.send(
            to="user@example.com",
            subject="Test",
            body="Hello"
        )
        assert result.success is True

Setup and Teardown Methods

Rustest automatically calls setup_method() and teardown_method() for plain test classes, matching pytest behavior. These methods run before and after each test method respectively:

class TestWithSetup:
    def setup_method(self):
        """Called before each test method."""
        self.connection = create_connection()
        self.data = []

    def teardown_method(self):
        """Called after each test method, even if the test fails."""
        self.connection.close()

    def test_insert(self):
        self.connection.insert({"key": "value"})
        assert self.connection.count() == 1

    def test_empty(self):
        assert self.connection.count() == 0

teardown_method() runs in a finally block, so it is guaranteed to execute even when the test raises an exception. This ensures resources are always cleaned up.

Class-Method Fixtures and Instance Sharing

When you define a @pytest.fixture (or @fixture) as a method inside a test class, the fixture method shares the same class instance as the test method that uses it. This means self refers to the same object in both the fixture and the test:

from rustest import fixture

class TestUserService:
    @fixture
    def service(self):
        """This fixture shares `self` with the test method."""
        self.service_instance = UserService()
        return self.service_instance

    def test_service_available(self, service):
        # self.service_instance was set by the fixture on the same instance
        assert self.service_instance is service

This is particularly useful when fixtures need to store state on the instance that test methods can access, or when multiple fixtures on the same class need to coordinate through shared instance attributes.

Best Practices

Keep Classes Focused

Each class should test a single component or feature:

# Good - focused on one component
class TestShoppingCart:
    def test_add_item(self):
        pass

    def test_remove_item(self):
        pass

    def test_calculate_total(self):
        pass

# Less ideal - testing multiple components
class TestEverything:
    def test_cart_add(self):
        pass

    def test_user_login(self):
        pass

    def test_payment_process(self):
        pass

Use Descriptive Class Names

# Good - clear what's being tested
class TestUserRegistration:
    pass

class TestPasswordReset:
    pass

# Less clear
class TestUser:
    pass

class TestStuff:
    pass

Don't Overuse Class Scope

Use class-scoped fixtures only when necessary:

from rustest import fixture

def create_expensive_connection():
    return {"status": "connected"}

# Good - expensive setup worth sharing
@fixture(scope="class")
def database_connection():
    return create_expensive_connection()

# Unnecessary - simple data doesn't benefit from class scope
@fixture(scope="class")  # Should be function scope
def sample_number():
    return 42

def test_with_db(database_connection):
    assert database_connection["status"] == "connected"

def test_with_number(sample_number):
    assert sample_number == 42

Combine with conftest.py

Use conftest.py for fixtures shared across multiple classes:

# conftest.py
from rustest import fixture

@fixture
def api_client():
    return APIClient()

# test_users.py
class TestUsers:
    def test_get_user(self, api_client):
        pass

# test_posts.py
class TestPosts:
    def test_get_post(self, api_client):
        pass

When to Use Test Classes

Use test classes when:

  • You have multiple related tests
  • You want to share fixtures across several tests
  • You want to group tests logically

Use standalone functions when:

  • You have a single test
  • Tests are independent and don't share setup
  • You prefer simplicity

Both approaches are valid and can be mixed in the same project!

Next Steps