Skip to main content
ANVISoftware Solutions
Lesson 3 of 13Intermediate14 min

Arrange, Act, Assert

By the end of this lesson

Structure tests so a failure points to a single cause.

Every test does three things in the same order: set up the situation, perform the action, check the result. Arrange, act, assert.

The convention is not about tidiness. It is about what happens when the test fails. A test with one action has one possible culprit, so the failure message is a diagnosis. A test with three actions tells you something broke somewhere in a sequence, and you are back to reading code.

What belongs in each part:

Arrange
Everything the action needs: the object under test, its inputs, any stand-ins for dependencies. No assertions here. If arranging is long and awkward, that is usually the design telling you something.
Act
One call. The thing whose behaviour this test is about. If you cannot point at a single line and say "this is the act", the test is doing too much.
Assert
Checks on the outcome of that one call. Several assertions are fine when they describe one outcome from different angles — three fields of the same returned order, for instance.

One action, one cause

A test that cannot tell you what broke
C#
[Fact]
public void OrderService_WorksCorrectly()
{
    var repository = new InMemoryOrderRepository();
    var service = new OrderService(repository);

    var order = service.CreateOrder("E-1042", 3, 25m);
    service.AddDiscount(order.Id, 10);
    service.Submit(order.Id);

    Assert.Equal(67.50m, repository.Find(order.Id)!.Total);
    Assert.Equal(OrderStatus.Submitted, repository.Find(order.Id)!.Status);
}
  • Three actions: create, discount, submit. When the total is wrong, the fault could be in any of them, or in how they interact.
  • The name says nothing. "WorksCorrectly" describes an aspiration, not a scenario.
  • It also fails for unrelated reasons. A change to how submission works breaks a test whose interesting assertion is about arithmetic.
The same coverage, split so failures are readable
C#
[Fact]
public void CreateOrder_WithThreeItemsAtTwentyFive_SetsTotalToSeventyFive()
{
    // Arrange
    var repository = new InMemoryOrderRepository();
    var service = new OrderService(repository);

    // Act
    Order order = service.CreateOrder("E-1042", quantity: 3, unitPrice: 25m);

    // Assert
    Assert.Equal(75m, order.Total);
}

[Fact]
public void AddDiscount_WithTenPercent_ReducesTotalByTenPercent()
{
    // Arrange
    var repository = new InMemoryOrderRepository();
    var service = new OrderService(repository);
    Order order = service.CreateOrder("E-1042", quantity: 3, unitPrice: 25m);

    // Act
    service.AddDiscount(order.Id, 10);

    // Assert
    Assert.Equal(67.50m, repository.Find(order.Id)!.Total);
}
  • Each test has exactly one act. When the second one fails, the discount calculation is the suspect and nothing else is.
  • Creating the order has moved into the arrange block of the second test. It is still setup, not the behaviour under examination — the same call can be an act in one test and arrangement in another.
  • Named arguments on CreateOrder mean the reader does not have to remember whether quantity or price comes first.
  • The comments are optional once the shape is habitual. Many teams drop them and rely on blank lines. Keep them while the pattern is new.

Two vocabularies for the same structure. You will meet both:

 Arrange / Act / AssertGiven / When / Then
Where it comes fromUnit testing practice, phrased from the test author's sideBehaviour-driven development, phrased as a specification
SetupArrange — build the objectsGiven — describe the starting state
ActionAct — call the methodWhen — the event occurs
CheckAssert — compare against an expected valueThen — the expected outcome holds
Reads best forUnit tests of a classTests that describe a business rule to non-developers

When arranging gets painful

If the arrange block runs to twenty lines and constructs six objects, the test is reporting a design problem rather than being badly written.

A class that needs that much scaffolding has too many dependencies, or it is reaching for things it should have been handed. The fix belongs in the production code more often than in the test. The next module covers the two tools that legitimately shorten setup: replacing awkward dependencies, and building test data through helpers.

Several inputs, still one act per case
C#
[Theory]
[InlineData(100, 0, 100)]
[InlineData(100, 10, 90)]
[InlineData(100, 100, 0)]
public void ApplyDiscount_ReducesPriceByThePercentageGiven(
    decimal price, int percentage, decimal expected)
{
    var calculator = new PriceCalculator();

    decimal result = calculator.ApplyDiscount(price, percentage);

    Assert.Equal(expected, result);
}
  • [Theory] marks a test that takes parameters; each [InlineData] supplies one set.
  • The runner treats each row as a separate test, so the output names the failing row rather than the whole group.
  • The structure inside is unchanged: arrange, one act, one assert. A theory is a family of tests, not a loop.
  • Keep theories to cases that genuinely differ only in data. If a row needs different arrangement, it is a different test.

Summary

  • Arrange, act, assert is a structure for making failures diagnosable, not a formatting rule
  • One action per test is the constraint that matters; several assertions about that one outcome are fine
  • Given/when/then describes the same shape in specification language
  • A long, awkward arrange block is usually a design signal rather than a test problem
  • Use [Theory] with [InlineData] for many inputs so each case is reported separately

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Try it yourself

Split a bloated test

Take the OrderService_WorksCorrectly test from this lesson and rewrite it as three tests: one for the total on creation, one for the effect of a discount, and one for the status after submission.

Name each one so a failure list alone tells you what broke.

Show solution

The third test arranges a created order, acts by submitting it, and asserts on status only. It says nothing about totals, so a pricing change cannot break it.

Notice that setup is now repeated across the three tests. That duplication is acceptable and often preferable — a reader can understand each test without scrolling. When it becomes genuinely heavy, extract a helper that returns a ready-made order rather than moving state into fields that all tests share.

C#
[Fact]
public void Submit_WhenOrderIsDraft_SetsStatusToSubmitted()
{
    // Arrange
    var repository = new InMemoryOrderRepository();
    var service = new OrderService(repository);
    Order order = service.CreateOrder("E-1042", quantity: 3, unitPrice: 25m);

    // Act
    service.Submit(order.Id);

    // Assert
    Assert.Equal(OrderStatus.Submitted, repository.Find(order.Id)!.Status);
}

Think about it

Is this one action?

A test arranges an order, calls Submit, then asserts on the order's status, its submitted timestamp, and the fact that a confirmation record now exists.

Three assertions. Is this test doing too much?

Show solution

No. There is one act, and the three assertions all describe the outcome of that single call. If any of them fails, Submit is the suspect.

There is a judgement call underneath, though: if the confirmation record is produced by a separate collaborator, asserting on it here couples this test to that collaborator's behaviour. A defensible alternative is to assert on status and timestamp here, and cover the confirmation in its own test. Both answers are reasonable; what matters is that you can name what each test is responsible for.

Saved in this browser only.