Unit testing behaviour classes
Dec 24, 2016
2 minute read

At the start of a project most unit tests are written in an Arrange-Act-Assert format:

[Test]
public void shouldIdentifyInvalidEmails() {
    // Arrange
    var emailValidator = new EmailValidator();
    
    // Act
    var result = emailValidator.validate("not an email address");
    
    // Assert
    Assert.isFalse(result);
}

This works well when testing individual units but can become verbose during integration testing or when testing entire modules. Behaviour classes can solve this issue. A behaviour class is a facade for a unit whose API is tailored towards unit testing. I tend to design behaviour class methods with a BDD-style naming scheme (Given-When-Then) so they dovetail into existing BDD testing libraries.

For example if we wanted to test a SavingsAccount class:

public class SavingsAccount() {
    public int Balance { get; private set; }
    private int isClosed;
    
    public SavingsAccount(int openingBalance) {
        Balance = openingBalance;
    }
    
    public int Deposit(int amount) {
        if(isClosed) {
            throw new InvalidOperationException(
                "Can't deposit into a closed account.");
        }
        
        Balance += amount;
    }
    
    public void Close() {
        isClosed = true;
    }

A behaviour class might look like:

public class SavingsAccountBehaviour() {
    private SavingsAccount account;
    private InvalidOperationException depositError;
    
    public void GivenIHaveAccountWithOpeningBalance(int openingBalance) {
        account = new SavingsAccount(openingBalance);
    }
    
    public void GivenTheAccountIsClosed() {
        account.Close();
    }
    
    public void WhenIDeposit(int amount) {
        try {
            account.Deposit(amount);
        } catch(InvalidOperationException e) {
            depositError = e;
        }
    }
    
    public void ThenThereIsADepositError(string expectedMessage) {
        Assert.isNotNull(depositError);
        Assert.areEqual(expectedMessage, depositError.Message);
    }
    
    public void ThenTheBalanceIs(int expectedBalance) {
        Assert.areEqual(expectedBalance, account.Balance);
    }
}

The unit test might look like this if I were using the behaviours class and a BDD testing library:

[Test]
public void shouldNotAcceptDepositsToClosedAccounts() {
    var acct = new SavingsAccountBehaviour();
    
    this.Given(x => acct.GivenIHaveAccountWithOpeningBalance(42))
        .Given(x => acct.GivenTheAccountIsClosed())
        .When(x => acct.WhenIDeposit(100))
        .Then(x => acct.ThenThereIsADepositError(
            "Can't deposit into a closed account."))
        .And(x => acct.ThenTheBalanceIs(42))
        .BDDfy();
}

By extracting the behaviours out into a separate class they can be reused throughout the test suite. Behaviour classes work well for integration tests. They can accept other behaviour classes as dependencies so that entire modules can be orchestrated using an API that is tailored for testing clarity.

Not a substitute for poor APIs

Make sure you’re not using behaviour classes to paper over poor APIs. Instead, refactor the APIs to make the simpler AAA-style tests viable.


Back to posts