DEV Community

Cover image for Avoid Inheritance For Test Classes
Jan Van Ryswyck
Jan Van Ryswyck

Posted on • Originally published at principal-it.eu on

Avoid Inheritance For Test Classes

Using inheritance for test classes is not a desirable thing as it introduces a number of issues. An abstract base class quite often originates from the desire to share and reuse some code with a number of derived test classes. Maintainable and readable test code should exhibit a nice balance between the DRY principle and the DAMP principle. This balance gets disturbed whenever we introduce a base class for other test classes to derive from, just in the name of DRY. Let’s have a look at an example to demonstrate this.

public class BankCard
{
    public bool Blocked { get; private set; }

    internal BankCard(bool blocked)
    {
        Blocked = blocked;
    }

    public static BankCard IssueNewBankCard()
    {
        return new BankCard(false);
    }

    public void ReportStolen()
    {
        Blocked = true;
    }

    public void Expire()
    {
        Blocked = true;
    }

    public void MakePayment(ActiveAccount fromAccount, ActiveAccount toAccount, double amount)
    {
        if(Blocked)
            throw new InvalidOperationException("Making payment is not allowed.");

        fromAccount.Withdraw(amount);
        toAccount.Deposit(amount);
    }
}

Enter fullscreen mode Exit fullscreen mode

The Subject Under Test of this example is the part of a domain model that revolves around banking and payments. Here we have a simple BankCard class which can be used to make payments. Obviously a bank card can also expire or reported stolen. In that case the bank card becomes blocked. This implies that no more payments can be made.

[Specification]
public class When_issuing_a_new_bank_card
{
    [Because]
    public void Of()
    {
        _result = BankCard.IssueNewBankCard();
    }

    [Observation]
    public void Then_the_bank_card_should_be_active()
    {
        _result.Blocked.Should_be_false();
    }

    private BankCard _result;
}

[Specification]
public class When_a_bank_card_is_reported_stolen
    : Bank_card_specification
{
    [Because]
    public void Of()
    {
        SUT.ReportStolen();
    }

    [Observation]
    public void Then_the_bank_card_should_be_blocked()
    {
        SUT.Blocked.Should_be_true();
    }
}

[Specification]
public class When_a_bank_card_is_expired
    : Bank_card_specification
{
    [Because]
    public void Of()
    {
        SUT.Expire();
    }

    [Observation]
    public void Then_the_bank_card_should_be_blocked()
    {
        SUT.Blocked.Should_be_true();
    }
}

[Specification]
public class When_making_a_payment
    : Bank_card_payment_specification
{
    [Because]
    public void Of()
    {
        SUT.MakePayment(FromAccount, ToAccount, 354.76);        
    }

    [Observation]
    public void Then_the_specified_amount_should_be_withdrawn_from_the_source_account()
    {
        FromAccount.Balance.Should_be_equal_to(1645.24);
    }

    [Observation]
    public void Then_the_specified_amount_should_be_deposited_to_the_target_account()
    {
        ToAccount.Balance.Should_be_equal_to(1354.76);
    }
}

[Specification]
public class When_making_a_payment_using_a_blocked_bank_card
    : Bank_card_payment_specification
{
    [Because]
    public void Of()
    {
        _makePayment = () => SUTBlocked.MakePayment(FromAccount, ToAccount, 162.88);
    }

    [Observation]
    public void Then_the_payment_should_not_be_allowed()
    {
        _makePayment.Should_throw_an<InvalidOperationException>();
    }

    private Action _makePayment;
}

Enter fullscreen mode Exit fullscreen mode

Here we have the implementation of the tests that exercise the functionality provided by the BankCard class. Notice that some of these tests derive from the Bank_card_specification base class, while others derive from the Bank_card_payment_specification base class. Let’s consider the fourth test scenario, which performs a payment for an amount of 354.76 from one account to another account. The new balances of these two accounts, 1645.24 and 1354.76 respectively, are verified after the payment has been made. However, it’s quite difficult to determine whether these values are correct just by reading the code of the test. This is because we’re missing an important part of the context that is relevant for this test scenario, which lives in the base class of the test. So in order to visually verify the correctness of the test, we also have to make a switch to a different location in the code base.

public abstract class Bank_card_specification
{
    [Establish]
    public void BaseContext()
    {
        SUT = Example.BankCard();
        SUTBlocked = Example.BankCard().AsBlocked();
    }

    protected BankCard SUT { get; private set; }
    protected BankCard SUTBlocked { get; private set; }
}

public abstract class Bank_card_payment_specification : Bank_card_specification
{
    [Establish]
    public void PaymentContext()
    {
        FromAccount = Example.ActiveAccount()
            .WithAccountName("From account")
            .WithBalance(2000);

        ToAccount = Example.ActiveAccount()
            .WithAccountName("To account")
            .WithBalance(1000);
    }

    protected ActiveAccount FromAccount { get; private set; }
    protected ActiveAccount ToAccount { get; private set; }
}

Enter fullscreen mode Exit fullscreen mode

This is how the base classes for the tests have been implemented. Here we find that the balance of the FromAccount is 2000, while the balance of the ToAccount is 1000. When switching back to the implementation of the test that verifies the payment, the new balances of these two accounts start to make more sense.

By moving test code to a base class, we’ve actually made it more difficult to comprehend what a particular test scenario is all about. It negatively impacts the readability and maintainability of these tests. A developer reading this implementation has to make a number of context switches between the test class and the abstract base class. These context switches result in a mental overhead.

The tests are coupled to the properties of the base class, which is also referred to as Subclass coupling. This makes it more difficult to move the tests around in the code base when needed.

Let’s get rid of these base classes.

[Specification]
public class When_issuing_a_new_bank_card
{
    [Because]
    public void Of()
    {
        _result = BankCard.IssueNewBankCard();
    }

    [Observation]
    public void Then_the_bank_card_should_be_active()
    {
        _result.Blocked.Should_be_false();
    }

    private BankCard _result;
}

[Specification]
public class When_a_bank_card_is_reported_stolen
{
    [Establish]
    public void Context()
    {
        _sut = Example.BankCard();
    }

    [Because]
    public void Of()
    {
        _sut.ReportStolen();
    }

    [Observation]
    public void Then_the_bank_card_should_be_blocked()
    {
        _sut.Blocked.Should_be_true();
    }

    private BankCard _sut;
}

[Specification]
public class When_a_bank_card_is_expired
{
    [Establish]
    public void Context()
    {
        _sut = Example.BankCard();
    }

    [Because]
    public void Of()
    {
        _sut.Expire();
    }

    [Observation]
    public void Then_the_bank_card_should_be_blocked()
    {
        _sut.Blocked.Should_be_true();
    }

    private BankCard _sut;
}

[Specification]
public class When_making_a_payment
{
    [Establish]
    public void Context()
    {
        _fromAccount = Example.ActiveAccount()
            .WithAccountName("From account")
            .WithBalance(2000);

        _toAccount = Example.ActiveAccount()
            .WithAccountName("To account")
            .WithBalance(1000);

        _sut = Example.BankCard();
    }

    [Because]
    public void Of()
    {
        _sut.MakePayment(_fromAccount, _toAccount, 354.76);        
    }

    [Observation]
    public void Then_the_specified_amount_should_be_withdrawn_from_one_account()
    {
        _fromAccount.Balance.Should_be_equal_to(1645.24);
    }

    [Observation]
    public void Then_the_specified_amount_should_be_deposited_to_another_account()
    {
        _toAccount.Balance.Should_be_equal_to(1354.76);
    }

    private ActiveAccount _fromAccount;
    private ActiveAccount _toAccount;
    private BankCard _sut;
}

[Specification]
public class When_making_a_payment_using_a_blocked_bank_card
{
    [Establish]
    public void Context()
    {
        _fromAccount = Example.ActiveAccount()
            .WithAccountName("From account")
            .WithBalance(2000);

        _toAccount = Example.ActiveAccount()
            .WithAccountName("To account")
            .WithBalance(1000);

        _sut = Example.BankCard().AsBlocked();
    }

    [Because]
    public void Of()
    {
        _makePayment = () => _sut.MakePayment(_fromAccount, _toAccount, 162.88);
    }

    [Observation]
    public void Then_the_payment_should_not_be_allowed()
    {
        _makePayment.Should_throw_an<InvalidOperationException>();
    }

    private ActiveAccount _fromAccount;
    private ActiveAccount _toAccount;
    private BankCard _sut;
    private Action _makePayment;
}

Enter fullscreen mode Exit fullscreen mode

All the test scenarios are now self-containing. By simultaneously applying the DRY principle as well as the DAMP principle we achieve more readability and better maintainability for our solitary tests.

There’s a widespread misconception amongst developers that inheritance is a cheap way to add some behaviour to a base class, so that one or more derived classes can benefit from that behaviour. However, that’s not truly the point. The point of inheritance is to provide polymorphic behaviour. It is definitely not the right tool for reusing code. We should use composition for that instead of inheritance.

According to Wikipedia:

“ Polymorphism is the provision of a single interface to entities of different types or the use of a single symbol to represent multiple different types.

This is something entirely different from just slapping some duplicate code on a base class and calling it a day. As the Gang of Four already expressed in their book Design Patterns: favour composition over class inheritance. This applies to both production code and test code.

Top comments (0)