Testing Basics¶
Now that you've written your first test, let's explore the fundamental concepts that make testing powerful.
The Anatomy of a Test¶
Every test follows a simple pattern called Arrange-Act-Assert (AAA):
def test_user_signup():
# ARRANGE: Set up the test data
email = "alice@example.com"
password = "secure_password"
# ACT: Do the thing you're testing
user = signup(email, password)
# ASSERT: Check the results
assert user.email == email
assert user.is_active is True
Let's break this down:
1. Arrange (Setup)¶
Prepare everything you need for the test:
This might include: - Creating test data - Setting up database connections - Configuring mock objects - Preparing input values
2. Act (Execute)¶
Run the code you're testing:
This is usually one line—the specific function or method you're testing.
3. Assert (Verify)¶
Check that the results are correct:
If all assertions pass, the test succeeds. If any fail, the test fails and shows you why.
Keep it simple
Each test should focus on one specific behavior. If you're testing too many things, split it into multiple tests.
Types of Assertions¶
Assertions are how you verify correctness. Here are the most common patterns:
Equality checks¶
def test_calculations():
assert 2 + 2 == 4
assert "hello".upper() == "HELLO"
assert [1, 2, 3] == [1, 2, 3]
Boolean checks¶
def test_boolean_conditions():
assert True
assert not False
assert user.is_admin is True
assert result is not None
Membership checks¶
def test_membership():
assert "hello" in "hello world"
assert 5 in [1, 2, 3, 4, 5]
assert "name" in user_dict
Comparison checks¶
Type checks¶
def test_types():
assert isinstance(result, int)
assert isinstance(user, User)
assert type(data) is dict
Testing for Errors¶
Sometimes you want your code to raise an error. Use raises():
from rustest import raises
def test_invalid_email():
with raises(ValueError):
signup("not-an-email", "password")
This test passes if ValueError is raised. If no error occurs (or a different error occurs), the test fails.
You can also check the error message:
def test_error_message():
with raises(ValueError, match="Invalid email format"):
signup("not-an-email", "password")
The test only passes if:
1. A ValueError is raised
2. The error message contains "Invalid email format"
Numeric Comparisons with Tolerance¶
Floating point math is imprecise. Use approx() for tolerant comparisons:
from rustest import approx
def test_floating_point():
result = 0.1 + 0.2
assert result == approx(0.3) # Works!
You can specify the tolerance:
This works with:
- Single numbers: approx(3.14)
- Lists: approx([1.1, 2.2, 3.3])
- Dictionaries: approx({"x": 1.1, "y": 2.2})
- Complex numbers: approx(1.1 + 2.2j)
What Makes a Good Test?¶
✅ Independent¶
Each test should run independently. One test shouldn't depend on another:
# ❌ BAD: Tests depend on each other
user = None
def test_create_user():
global user
user = signup("alice@example.com", "password")
assert user is not None
def test_user_login():
# This fails if test_create_user didn't run first!
result = login(user)
assert result.success
# ✅ GOOD: Each test is independent
def test_create_user():
user = signup("alice@example.com", "password")
assert user is not None
def test_user_login():
# Set up everything we need
user = signup("alice@example.com", "password")
result = login(user)
assert result.success
✅ Fast¶
Tests should run quickly so you can run them often:
# ❌ BAD: Slow test
def test_slow_operation():
time.sleep(5) # Don't do this!
assert calculate() == 42
# ✅ GOOD: Fast test
def test_fast_operation():
result = calculate() # Should be instant
assert result == 42
If you must have slow tests (like API calls), mark them so you can skip them:
from rustest import mark
@mark.slow
def test_external_api():
response = call_external_api()
assert response.status == 200
Then run fast tests only:
✅ Readable¶
Someone else (or future you) should understand what the test does:
# ❌ BAD: Unclear test
def test_x():
a = f(1, 2)
assert a == 3
# ✅ GOOD: Clear test
def test_add_function_sums_two_numbers():
result = add(1, 2)
assert result == 3
Good test names answer: "What does this test verify?"
✅ Focused¶
Test one thing at a time:
# ❌ BAD: Testing too much
def test_user_operations():
user = signup("alice@example.com", "password")
assert user is not None
login_result = login(user)
assert login_result.success
profile = get_profile(user)
assert profile.name == "Alice"
# ✅ GOOD: Separate focused tests
def test_signup_creates_user():
user = signup("alice@example.com", "password")
assert user is not None
def test_login_succeeds_with_valid_credentials():
user = signup("alice@example.com", "password")
result = login(user)
assert result.success
def test_profile_shows_user_name():
user = signup("alice@example.com", "password")
profile = get_profile(user)
assert profile.name == "Alice"
When one test fails, you immediately know what broke.
Test Organization Strategies¶
Group related tests¶
# test_user_auth.py
def test_signup_with_valid_email():
# ...
def test_signup_with_invalid_email():
# ...
def test_login_with_correct_password():
# ...
def test_login_with_wrong_password():
# ...
Use descriptive file names¶
tests/
├── test_authentication.py # All auth-related tests
├── test_database.py # Database tests
├── test_api.py # API endpoint tests
└── test_utils.py # Utility function tests
Test edge cases¶
Don't just test the happy path:
def test_add_positive_numbers():
assert add(2, 3) == 5
def test_add_negative_numbers():
assert add(-2, -3) == -5
def test_add_zero():
assert add(0, 5) == 5
assert add(5, 0) == 5
def test_add_large_numbers():
assert add(1_000_000, 2_000_000) == 3_000_000
Think about:
- Empty inputs ([], "", None)
- Zero and negative numbers
- Very large values
- Invalid inputs
- Boundary conditions
Running and Filtering Tests¶
Run all tests:
Run specific tests:
# Run one file
rustest tests/test_auth.py
# Run tests matching a pattern
rustest -k "login" # Runs all tests with "login" in the name
# Run tests in a directory
rustest tests/unit/
See detailed output:
What's Next?¶
You now understand the fundamentals of testing! Ready to level up?
Make Tests Reusable¶
Fixtures let you reuse setup code across multiple tests. Instead of copying the same setup everywhere, define it once and use it everywhere.
Test Multiple Cases Efficiently¶
Test the same logic with different inputs without writing repetitive tests.
Organize Larger Test Suites¶
Learn how to structure tests for real projects with marks, test classes, and more.