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¶
- Fixtures - Learn more about fixture scopes
- Marks & Skipping - Apply marks to test classes
- Writing Tests - General testing patterns