テストクラスの継承を避ける


テストクラスの継承を使用することは、いくつかの問題を導入するので望ましいものではありません.抽象基底クラスは、しばしばいくつかの派生したテストクラスでいくつかのコードを共有して、再利用する欲求から生じます.保守可能で読みやすいテストコードは、乾式原理と湿った原理の間に良いバランスを示すべきです.他のテストクラスの基底クラスを導入するときはいつでも、このバランスは乱されます.これを示す例を見てみましょう.
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);
    }
}

この例のテスト対象は、銀行と支払いを中心とするドメインモデルの一部です.ここでは、支払いをするために使用できる簡単な銀行カードクラスがあります.銀行カードも明らかに盗まれたり盗まれたりした.その場合、銀行カードはブロックします.これは、これ以上の支払いを行うことができることを意味します.
[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;
}

ここでは、BankCardクラスによって提供される機能を実行するテストの実装を行います.これらのテストのいくつかはBankSum CardHand仕様ベースクラスから派生していますが、他のものはBankSay CardCount PaymentRank仕様ベースクラスから派生しています.つのアカウントから別のアカウントへの354.76の金額の支払いを行う4番目のテストシナリオを考えてみましょう.これらの2つの口座(1645.24と1354.76)の新しい残高は、支払いがなされたあと、確かめられます.しかし、テストのコードを読むだけで、これらの値が正しいかどうかを判断するのはかなり難しい.これは、テストの基本クラスに住んでいるこのテストシナリオに関連するコンテキストの重要な部分が見つからないためです.テストの正当性を視覚的に確認するために、コードベースの別の場所に切り替える必要があります.
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; }
}

これは、テストの基本クラスが実装されている方法です.ここでは、FlomAccountのバランスは2000であり、一方、アカウントのバランスは1000です.支払いを確認するテストの実施に戻るとき、これらの2つの口座の新しい残高は、より意味をなし始めます.
ベースクラスにテストコードを移動することによって、我々は実際にそれが特定のテストシナリオが何であるかについて理解するのがより難しくしました.これらのテストの可読性と保守性に悪影響を与えます.この実装を読んでいる開発者は、テストクラスと抽象基底クラスの間で多くのコンテキストスイッチを作成しなければなりません.これらのコンテキストスイッチは、精神的なオーバーヘッドをもたらす.
テストは基底クラスのプロパティに結合され、基底クラスのサブクラス結合とも呼ばれる.これにより、必要に応じてコードベースでテストを動かすことがより困難になります.
これらの基本クラスを取り除きましょう.
[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;
}

すべてのテストシナリオは、自己を含んでいる.湿った原理と同様に乾式原理を同時に適用することによって、我々は我々の孤独なテストのためにより読みやすくてより保守性を達成します.
継承がベースクラスに何らかの振る舞いを加える安価な方法であるという開発者の間には広範囲にわたる誤解があります.そのため、1つ以上の派生クラスがその振る舞いから利益を得ることができます.しかし、それは本当にポイントではありません.遺伝のポイントは、多形のふるまいを提供することです.それは間違いなくコードを再利用するための適切なツールではありません.私たちは、遺産の代わりにそれのために構成を使うべきです.
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.


これは、基本クラスで重複するコードをスラッピングして一日呼び出すだけで全く異なっています.すでに彼らの本Design Patternsで表現された4人のギャングとして:クラス継承に対する好意的な構成.これは、生産コードとテストコードの両方に適用されます.