TradingAgents/.claude/skills/testing-guide/arrange-act-assert.md

8.9 KiB

Arrange-Act-Assert Pattern Guide

Purpose: The AAA pattern is a standard structure for writing clear, maintainable tests.

When to use: For all unit and integration tests to ensure consistent, readable test structure.


The AAA Pattern

The Arrange-Act-Assert (AAA) pattern divides tests into three distinct phases:

  1. Arrange: Set up test data, mock dependencies, configure initial state
  2. Act: Execute the code under test
  3. Assert: Verify the expected outcomes

This structure makes tests self-documenting and easy to understand.


Arrange Phase

The Arrange phase prepares everything needed for the test.

Setting Up Test Data

def test_user_creation():
    """Test user creation with valid data."""
    # Arrange
    user_data = {
        "username": "testuser",
        "email": "test@example.com",
        "password": "secure_password"
    }

    # Act
    user = create_user(user_data)

    # Assert
    assert user.username == "testuser"
    assert user.email == "test@example.com"

Mocking Dependencies

def test_api_call_with_mock():
    """Test API call with mocked HTTP client."""
    # Arrange
    mock_client = Mock()
    mock_client.get.return_value = {
        "status": "success",
        "data": {"id": 1, "name": "Test"}
    }
    api = APIService(client=mock_client)

    # Act
    result = api.fetch_user(user_id=1)

    # Assert
    assert result["name"] == "Test"
    mock_client.get.assert_called_once_with("/users/1")

Configuring Initial State

def test_shopping_cart_total():
    """Test shopping cart total calculation."""
    # Arrange
    cart = ShoppingCart()
    cart.add_item("Product A", price=10.00, quantity=2)
    cart.add_item("Product B", price=5.00, quantity=3)

    # Act
    total = cart.calculate_total()

    # Assert
    assert total == 35.00

Act Phase

The Act phase executes the code under test. Keep this phase minimal - ideally one line.

Single Action

def test_string_uppercase():
    """Test string conversion to uppercase."""
    # Arrange
    input_string = "hello world"

    # Act
    result = input_string.upper()

    # Assert
    assert result == "HELLO WORLD"

Method Call with Parameters

def test_calculate_discount():
    """Test discount calculation."""
    # Arrange
    calculator = DiscountCalculator()
    original_price = 100.00
    discount_rate = 0.20

    # Act
    final_price = calculator.apply_discount(original_price, discount_rate)

    # Assert
    assert final_price == 80.00

Exception Testing

def test_division_by_zero():
    """Test that division by zero raises error."""
    # Arrange
    calculator = Calculator()

    # Act & Assert (combined for exception testing)
    with pytest.raises(ZeroDivisionError):
        calculator.divide(10, 0)

Assert Phase

The Assert phase verifies the expected outcomes.

Simple Assertions

def test_list_append():
    """Test appending to list."""
    # Arrange
    my_list = [1, 2, 3]

    # Act
    my_list.append(4)

    # Assert
    assert len(my_list) == 4
    assert my_list[-1] == 4

Multiple Assertions

It's acceptable to have multiple assertions that verify different aspects of the outcome:

def test_user_registration():
    """Test user registration creates user correctly."""
    # Arrange
    registration_data = {
        "email": "newuser@example.com",
        "password": "secure123"
    }

    # Act
    user = register_user(registration_data)

    # Assert
    assert user.email == "newuser@example.com"
    assert user.is_active is True
    assert user.created_at is not None
    assert user.id is not None

Asserting Side Effects

def test_log_message_written():
    """Test that log message is written to file."""
    # Arrange
    logger = Logger("test.log")
    message = "Test log message"

    # Act
    logger.write(message)

    # Assert
    with open("test.log") as f:
        content = f.read()
        assert message in content

Asserting Mock Calls

def test_notification_sent():
    """Test that notification is sent to user."""
    # Arrange
    mock_notifier = Mock()
    service = UserService(notifier=mock_notifier)
    user = User(email="test@example.com")

    # Act
    service.notify_user(user, "Welcome message")

    # Assert
    mock_notifier.send.assert_called_once_with(
        to=user.email,
        message="Welcome message"
    )

Before and After Examples

Before: Unclear Test Structure

def test_order_processing():
    """Test order processing (unclear structure)."""
    order = Order()
    order.add_item(Item("Product", 10.00))
    payment = Payment(amount=10.00)
    assert process_order(order, payment) is True
    assert order.status == "completed"
    assert payment.status == "processed"

After: Clear AAA Structure

def test_order_processing():
    """Test order processing with payment."""
    # Arrange
    order = Order()
    order.add_item(Item("Product", 10.00))
    payment = Payment(amount=10.00)

    # Act
    result = process_order(order, payment)

    # Assert
    assert result is True
    assert order.status == "completed"
    assert payment.status == "processed"

AAA Pattern with Fixtures

Fixtures can handle the Arrange phase:

@pytest.fixture
def user_with_account():
    """Arrange: Create user with account."""
    user = User(username="testuser")
    account = Account(balance=100.00)
    user.account = account
    return user

def test_withdraw_money(user_with_account):
    """Test withdrawing money from account."""
    # Arrange (done by fixture)
    user = user_with_account
    withdrawal_amount = 25.00

    # Act
    result = user.account.withdraw(withdrawal_amount)

    # Assert
    assert result is True
    assert user.account.balance == 75.00

AAA Pattern with Parametrization

Combine AAA with parametrization:

@pytest.mark.parametrize("input_value,expected_output", [
    (0, "zero"),
    (1, "one"),
    (5, "five"),
    (10, "ten"),
])
def test_number_to_word(input_value, expected_output):
    """Test number to word conversion."""
    # Arrange
    converter = NumberConverter()

    # Act
    result = converter.to_word(input_value)

    # Assert
    assert result == expected_output

Common Mistakes

Mistake 1: Mixing Phases

# ❌ Bad: Arrange and Act mixed
def test_bad_structure():
    user = User("test")
    user.age = 25
    result = user.is_adult()
    user.name = "Test User"
    assert result is True
# ✅ Good: Clear phases
def test_good_structure():
    # Arrange
    user = User("test")
    user.age = 25
    user.name = "Test User"

    # Act
    result = user.is_adult()

    # Assert
    assert result is True

Mistake 2: Multiple Actions

# ❌ Bad: Multiple actions
def test_multiple_actions():
    # Arrange
    calculator = Calculator()

    # Act
    result1 = calculator.add(2, 3)
    result2 = calculator.multiply(4, 5)

    # Assert
    assert result1 == 5
    assert result2 == 20
# ✅ Good: One action per test
def test_addition():
    # Arrange
    calculator = Calculator()

    # Act
    result = calculator.add(2, 3)

    # Assert
    assert result == 5

def test_multiplication():
    # Arrange
    calculator = Calculator()

    # Act
    result = calculator.multiply(4, 5)

    # Assert
    assert result == 20

Mistake 3: Asserting in Arrange

# ❌ Bad: Assertions in arrange phase
def test_with_assertions_in_arrange():
    # Arrange
    user = create_user("test@example.com")
    assert user is not None  # Don't assert here

    # Act
    result = user.login("password")

    # Assert
    assert result is True
# ✅ Good: Only assert in assert phase
def test_without_assertions_in_arrange():
    # Arrange
    user = create_user("test@example.com")

    # Act
    result = user.login("password")

    # Assert
    assert user is not None
    assert result is True

Benefits of AAA Pattern

  1. Readability: Anyone can understand what the test does
  2. Maintainability: Easy to modify any phase independently
  3. Debugging: Quick to identify where test fails (arrange, act, or assert)
  4. Consistency: All tests follow same structure
  5. Self-documenting: Test structure tells the story

AAA Pattern Checklist

Before committing a test, verify:

  • Arrange phase sets up all necessary data and state
  • Act phase has minimal code (ideally one line)
  • Assert phase verifies expected outcomes
  • Phases are clearly separated (with comments or blank lines)
  • Test has descriptive name and docstring
  • No assertions in Arrange phase
  • No setup in Act phase
  • One primary action being tested

For more details: See SKILL.md for complete testing methodology and test-templates/ for working examples with AAA pattern.