Unit Tests and Arrange–Act–Assert
Before you begin: Methods, objects, exceptions, and assertions.
A unit test checks a small, focused piece of behavior under controlled conditions. The unit might be one function or several objects working together; it does not have to coincide with one class.
Arrange–Act–Assert is a way to organize that test. Establish the starting conditions, perform the operation, then compare the actual outcome with the expected one. Separating those steps helps a reader see what the test is claiming. 1
One promise worth testing
A completed quest awards twenty coins, once. The quest object below models only claiming: creation tells it whether completion has already occurred. This is a sequential in-memory example, not a server that prevents cheating or coordinates multiple database records.
public sealed class Quest
{
private readonly bool complete;
private bool claimed;
public Quest(bool complete) { this.complete = complete; }
public int ClaimReward()
{
if (!complete || claimed)
throw new InvalidOperationException("Reward unavailable.");
claimed = true;
return 20;
}
}
public static class Tests
{
public static void ClaimingACompletedQuestReturnsItsReward()
{
// Arrange
var quest = new Quest(complete: true);
// Act
int coins = quest.ClaimReward();
// Assert
if (coins != 20) throw new Exception("Expected twenty coins.");
}
public static void ASecondClaimIsRejected()
{
// Arrange: the first claim is part of this scenario's starting state.
var quest = new Quest(complete: true);
quest.ClaimReward();
// Act
bool rejected = false;
try { quest.ClaimReward(); }
catch (InvalidOperationException) { rejected = true; }
// Assert
if (!rejected) throw new Exception("A second reward was allowed.");
}
}Tests.ClaimingACompletedQuestReturnsItsReward();
Tests.ASecondClaimIsRejected();The first test observes the returned coins. The second observes a specific exception on the second attempt. Neither inspects the private claimed field: changing how the quest records a claim should not break the tests if its public behavior is unchanged.
The explicit throws keep the example independent of a framework. In a test project, use the framework’s equality and expected-exception assertions; those produce clearer failure reports than hand-written Boolean bookkeeping.
class Quest:
def __init__(self, complete):
self._complete = complete
self._claimed = False
def claim_reward(self):
if not self._complete or self._claimed:
raise RuntimeError("Reward unavailable.")
self._claimed = True
return 20
def test_claiming_a_completed_quest_returns_its_reward():
# Arrange
quest = Quest(complete=True)
# Act
coins = quest.claim_reward()
# Assert
assert coins == 20
def test_a_second_claim_is_rejected():
# Arrange
quest = Quest(complete=True)
quest.claim_reward()
# Act
rejected = False
try:
quest.claim_reward()
except RuntimeError:
rejected = True
# Assert
assert rejected, "A second reward was allowed."test_claiming_a_completed_quest_returns_its_reward()
test_a_second_claim_is_rejected()Each test creates a fresh quest. The successful first claim in the second test is setup: the behavior under examination is the next claim. A test framework’s expected-exception helper can replace the explicit flag while preserving that distinction.
Test the outcome, not your memory of the implementation
A test that only checks that no exception occurred would miss a reward of nineteen coins. A test that checks the private flag but never tries a second claim could miss a method that ignores the flag. Choose observations that express the promise.
An incomplete quest is a separate scenario and deserves another test. These two checks are not a complete specification. Their names make their scope visible so the missing scenario is easier to notice.
Keep setup local and believable
The quest does not need an email server, a real player account, or a database to demonstrate this rule. Supplying such infrastructure would introduce failure causes unrelated to reward eligibility.
That does not imply all collaborators must be replaced by mocks. Small deterministic objects can work together in a focused test. The boundary matters less than whether the test has controlled inputs, a clear claim, and a useful failure message.
Arrange–Act–Assert is a reading aid, not a requirement to decorate every test with three comments. When the steps are obvious, whitespace and names can do the work. What matters is that another programmer can identify the starting state, the operation being tested, and the reason the test passes.
Sources
- Microsoft — Unit testing best practices, on Arrange–Act–Assert and readable tests.
- Python — unittest, for framework-based equality and exception assertions.