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
[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.
[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 / Assert | Given / When / Then | |
|---|---|---|
| Where it comes from | Unit testing practice, phrased from the test author's side | Behaviour-driven development, phrased as a specification |
| Setup | Arrange — build the objects | Given — describe the starting state |
| Action | Act — call the method | When — the event occurs |
| Check | Assert — compare against an expected value | Then — the expected outcome holds |
| Reads best for | Unit tests of a class | Tests 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.
[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.
[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.