Avoid Inheritance For Test Classes
January 6, 2021Using 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);
}
}
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;
}
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; }
}
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;
}
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.— Wikipedia
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.
If you and your team want to learn more about how to write maintainable unit tests and get the most out of TDD practices, make sure to have look at our trainings and workshops or check out the books section. Feel free to reach out at info. @ principal-it .be
Jan Van Ryswyck
Thank you for visiting my blog. I’m a professional software developer since Y2K. A blogger since Y2K+5. Provider of training and coaching in XP practices. Curator of the Awesome Talks list. Past organizer of the European Virtual ALT.NET meetings. Thinking and learning about all kinds of technologies since forever.
Comments
Writing Maintainable
Unit Tests
Watch The Videos
Latest articles
-
Contract Tests - Parameterised Test Cases
June 28, 2023
-
Contract Tests - Abstract Test Cases
April 12, 2023
-
Contract Tests
February 1, 2023
-
The Testing Quadrant
June 15, 2022
-
Tales Of TDD: The Big Refactoring
February 2, 2022
Tags
- .NET
- ALT.NET
- ASP.NET
- Agile
- Announcement
- Architecture
- Behavior-Driven Development
- C++
- CQRS
- Clojure
- CoffeeScript
- Community
- Concurrent Programming
- Conferences
- Continuous Integration
- Core Skills
- CouchDB
- Database
- Design Patterns
- Domain-Driven Design
- Event Sourcing
- F#
- Fluent Interfaces
- Functional Programming
- Hacking
- Humor
- Java
- JavaScript
- Linux
- Microsoft
- NHibernate
- NoSQL
- Node.js
- Object-Relational Mapping
- Open Source
- Reading
- Ruby
- Software Design
- SourceControl
- Test-Driven Development
- Testing
- Tools
- Visual Studio
- Web
- Windows
Disclaimer
The opinions expressed on this blog are my own personal opinions. These do NOT represent anyone else’s view on the world in any way whatsoever.
About
Thank you for visiting my website. I’m a professional software developer since Y2K. A blogger since Y2K+5. Author of Writing Maintainable Unit Tests. Provider of training and coaching in XP practices. Curator of the Awesome Talks list. Thinking and learning about all kinds of technologies since forever.
Latest articles
Contract Tests - Parameterised Test Cases
Contract Tests - Abstract Test Cases
Contract Tests
The Testing Quadrant
Contact information
(+32) 496 38 00 82
info @ principal-it .be