Learnixo
Back to blog
AI Systemsintermediate

AAA Pattern — Arrange, Act, Assert for Clean, Readable Tests

How to apply the Arrange-Act-Assert pattern consistently in Clean Architecture .NET tests: structure, naming conventions, FluentAssertions, parameterized tests with Theory, and the common mistakes that make tests hard to maintain.

Asma Hafeez KhanMay 16, 20265 min read
Clean Architecture.NETTestingAAA PatternxUnitFluentAssertions
Share:𝕏

What the AAA Pattern Is

Every test has exactly three sections:

Arrange:  set up the system under test and its dependencies
Act:      invoke the operation being tested
Assert:   verify the outcome

The pattern makes tests scannable. Any developer can read a test and immediately locate what is being tested (Act) and what is being verified (Assert).


Basic Example

C#
[Fact]
public async Task Handle_valid_command_should_return_patient_id()
{
    // Arrange
    var patients   = new FakePatientRepository();
    var unitOfWork = new FakeUnitOfWork();
    var handler    = new CreatePatientCommandHandler(patients, unitOfWork);
    var command    = new CreatePatientCommand(
        Name:        "John Smith",
        DateOfBirth: new DateOnly(1985, 3, 15),
        MRN:         "MRN-001");

    // Act
    var result = await handler.Handle(command, CancellationToken.None);

    // Assert
    result.IsSuccess.Should().BeTrue();
    result.Value.Value.Should().NotBe(Guid.Empty);
}

Naming Conventions

Test names should read as a sentence describing the behavior:

C#
// Pattern: {Method}_{Condition}_{ExpectedOutcome}
public async Task Handle_when_mrn_already_exists_should_return_failure()
public async Task Handle_with_valid_data_should_save_to_repository()
public async Task AddPrescription_to_inactive_patient_should_return_error()
public void Create_patient_with_future_dob_should_fail()
public void INRReading_above_4_should_require_urgent_review()

// Alternative: given-when-then
public async Task Given_duplicate_mrn_when_creating_patient_then_returns_conflict_error()

Complete Handler Test Class

C#
// tests/Application.UnitTests/Patients/AddPrescriptionCommandHandlerTests.cs
public class AddPrescriptionCommandHandlerTests
{
    private readonly FakePatientRepository _patients;
    private readonly FakeUnitOfWork _unitOfWork;
    private readonly AddPrescriptionCommandHandler _sut;

    // Arrange (shared): the handler under test
    public AddPrescriptionCommandHandlerTests()
    {
        _patients  = new FakePatientRepository();
        _unitOfWork = new FakeUnitOfWork();
        _sut       = new AddPrescriptionCommandHandler(_patients, _unitOfWork);
    }

    [Fact]
    public async Task Handle_valid_command_should_add_prescription_and_return_id()
    {
        // Arrange
        var patient = Patient.Create("John Smith", new DateOnly(1985, 3, 15), "MRN-001").Value;
        _patients.Seed(patient);

        var command = new AddPrescriptionCommand(
            PatientId:     patient.Id,
            MedicationCode: "WARFARIN",
            DosageAmount:  5m,
            DosageUnit:    "mg",
            Frequency:     "Once daily");

        // Act
        var result = await _sut.Handle(command, CancellationToken.None);

        // Assert
        result.IsSuccess.Should().BeTrue();
        result.Value.Should().NotBe(PrescriptionId.Empty);
        _unitOfWork.SaveChangesCallCount.Should().Be(1);
    }

    [Fact]
    public async Task Handle_patient_not_found_should_return_not_found_error()
    {
        // Arrange
        var command = new AddPrescriptionCommand(
            PatientId:     PatientId.New(),   // ID that does not exist in the fake repo
            MedicationCode: "WARFARIN",
            DosageAmount:  5m,
            DosageUnit:    "mg",
            Frequency:     "Once daily");

        // Act
        var result = await _sut.Handle(command, CancellationToken.None);

        // Assert
        result.IsFailure.Should().BeTrue();
        result.Error.Should().Be(PatientErrors.NotFound);
        _unitOfWork.SaveChangesCallCount.Should().Be(0);
    }

    [Fact]
    public async Task Handle_inactive_patient_should_return_inactive_error()
    {
        // Arrange
        var patient = Patient.Create("Jane Doe", new DateOnly(1990, 7, 22), "MRN-002").Value;
        patient.Deactivate();
        _patients.Seed(patient);

        var command = new AddPrescriptionCommand(
            patient.Id, "ASPIRIN", 100m, "mg", "Once daily");

        // Act
        var result = await _sut.Handle(command, CancellationToken.None);

        // Assert
        result.IsFailure.Should().BeTrue();
        result.Error.Should().Be(PatientErrors.InactivePatient);
    }

    [Fact]
    public async Task Handle_duplicate_active_prescription_should_return_error()
    {
        // Arrange
        var patient = Patient.Create("Bob Jones", new DateOnly(1975, 11, 30), "MRN-003").Value;
        var existingRx = BuildPrescription("WARFARIN");
        patient.AddPrescription(existingRx);
        _patients.Seed(patient);

        var command = new AddPrescriptionCommand(
            patient.Id, "WARFARIN", 5m, "mg", "Once daily");   // same drug

        // Act
        var result = await _sut.Handle(command, CancellationToken.None);

        // Assert
        result.IsFailure.Should().BeTrue();
        result.Error.Should().Be(PatientErrors.DuplicateActivePrescription);
    }

    private static Prescription BuildPrescription(string code)
        => Prescription.Create(
            MedicationCode.Create(code).Value,
            Dosage.Create(5m, "mg").Value,
            "Once daily").Value;
}

Parameterized Tests With Theory

C#
// tests/Application.UnitTests/Domain/DosageTests.cs
public class DosageTests
{
    [Theory]
    [InlineData(500,  "mg")]
    [InlineData(0.5,  "mcg")]
    [InlineData(10,   "mL")]
    [InlineData(100,  "IU")]
    [InlineData(2,    "units")]
    public void Create_with_valid_values_should_succeed(decimal amount, string unit)
    {
        // Arrange + Act
        var result = Dosage.Create(amount, unit);

        // Assert
        result.IsSuccess.Should().BeTrue();
    }

    [Theory]
    [InlineData(0,    "mg",    "AmountMustBePositive")]
    [InlineData(-100, "mg",    "AmountMustBePositive")]
    [InlineData(100,  "",      "UnitRequired")]
    [InlineData(100,  "gallons", "InvalidUnit")]
    public void Create_with_invalid_values_should_fail_with_expected_error(
        decimal amount, string unit, string expectedErrorCode)
    {
        // Arrange + Act
        var result = Dosage.Create(amount, unit);

        // Assert
        result.IsFailure.Should().BeTrue();
        result.Error.Code.Should().Contain(expectedErrorCode);
    }
}

Avoid These Common Mistakes

C#
// ✗ Multiple Acts in one test — which one failed?
[Fact]
public async Task Bad_test()
{
    var r1 = await handler.Handle(command1, ct);
    var r2 = await handler.Handle(command2, ct);
    r1.IsSuccess.Should().BeTrue();
    r2.IsFailure.Should().BeTrue();
}
// Fix: one test per behavior

// ✗ Assertions in Arrange — you're testing setup code, not the handler
[Fact]
public async Task Bad_arrangement()
{
    var patient = Patient.Create(...).Value;
    patient.Name.Should().Be("John");   // ← this is not an Act
    // ...
}

// ✗ No clear separation — all three phases blended
[Fact]
public async Task Hard_to_read()
{
    var result = await new CreatePatientCommandHandler(
        new FakePatientRepository(),
        new FakeUnitOfWork())
        .Handle(new CreatePatientCommand("John", new DateOnly(1985,3,15), "MRN-001"), default);
    Assert.True(result.IsSuccess);
}
// Fix: separate Arrange, Act, Assert with blank lines and comments

PRO TIP — Test Data Builders

When test setup requires many fields, use a builder pattern to avoid repetitive inline setup:

C#
// tests/Builders/CreatePatientCommandBuilder.cs
public sealed class CreatePatientCommandBuilder
{
    private string _name        = "Default Patient";
    private DateOnly _dob       = new(1980, 1, 1);
    private string _mrn         = "MRN-DEFAULT";

    public CreatePatientCommandBuilder WithName(string name)     { _name = name; return this; }
    public CreatePatientCommandBuilder WithMRN(string mrn)       { _mrn  = mrn;  return this; }
    public CreatePatientCommandBuilder WithDOB(DateOnly dob)     { _dob  = dob;  return this; }

    public CreatePatientCommand Build() =>
        new CreatePatientCommand(_name, _dob, _mrn);
}

// In tests:
var command = new CreatePatientCommandBuilder()
    .WithName("John Smith")
    .WithMRN("MRN-999")
    .Build();

Key Takeaway

AAA is not just a naming convention — it is a test discipline that forces each test to verify exactly one behavior. When a test fails, you know which assertion failed and why. When you read a test, you know immediately what it is testing without reading the entire body. Good test names document the expected behavior of the system — they are the most up-to-date specification you have.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.