LoginSignup
2
1

More than 3 years have passed since last update.

Adaptive Codeまとめ:テスト

Posted at

はじめに

私は普段C#を使ったUnity開発に取り組んでいます。これまではどちらかと言えば、「良いコードを書きたい」というより、「バグがなくてきちっと動けばそれで良い」という考えでコードを書いてきたように思います。ただ、エンジニアとしてはそれではやはり不十分でしょう。そこで、『Adaptive Code ~ C#実践開発手法』を読んで勉強することにしました。

「アダプティブ」とは、コードを大幅に変更することなく、新しい要求や予想外のシナリオに対処する適応力のことです。

ただ、本書は基本的に.NET Frameworkでの開発を前提として書かれていますし1、文字数も多いので、自分にとって重要な箇所を抜き出してまとめながら学習を進めることは意味のあることだと思います。
今回のまとめは第5章の内容に当たります。

ユニットテスト

AAA

各ユニットテストは、以下の3つのセクションで構成される(AAAパターン)。

   ■ Arrange・・・テストの事前条件のセットアップ
   ■ Act・・・テストの対象となるアクションの実行
   ■ Assert・・・振る舞いが期待どおりであることの検証

あなたが作成するどのテストでも、このパターンに従い、他の人があなたのユニットテストを読んで理解できるようにすること。

以下は、顧客の口座の残高と取引を表すAccountクラスを用いて、AAAパターンに従って書いたテストコードの例(ここでは、テストツールとしてMSTestを使用しているが、本質的ではない)。

[TestClass]
public class AccountTest {
    [TestMethod]
    public void AddingTransactionChangeBalance() {
        // Arrange:今回の場合は単純で、Accountクラスの新しいインスタンスを作成するだけ
        var account = new Account();

        // Act:200mという金額を口座に入金
        account.AddTransaction(200m);

        // Assert:口座の金額が正しいかチェック
        Assert.AreEqual(200m, account.Balance);
    }
}

テストメソッドの名前はAddingTransactionChangesBalanceであり、口座で取引が発生するたびに、新しい取引を反映して口座の残高が変更されることを確認するという、このテストの意図を端的に表している。
以下、Arrange,Act,Assertについて少し詳しく説明する。


事前条件のセットアップ:Arrange

テストを必要とするアクションを実行するには、まず、テストの対象となるシナリオをセットアップしなければならない。テストによってはSUT(System Under Test)を作成するだけで済むことがある。SUTとは、あなたがテストするクラスのことで、このクラスの有効なインスタンスがなければ、そのメソッドをテストすることは不可能である。

テストの対象となるアクションの実行:Act

各テストのActセクションは、例えば1つのメソッドの呼び出しや、プロパティの設定や取得など、SUTとの1つのやり取りでのみ構成されていなければならない。これにより、テストの読み書きが単純になり、実行パスが明確になる。

期待値の検証:Assert

Assertセクションでは、テスト全体の成功または失敗が緑または赤のインジケーターで示される。この場合のアサーションは、実際の値と期待値とを比較すること(今回は等しいことを要求する)で、これは状態ベースのテストでよく使用されるアサーションである。状態ベースのテストとは、アサーションがSUTの状態に依存するテストのこと。
実際の値はAccountクラスのBalanceプロパティから取り出し、期待値は定数として定義する。つまり、期待値を事前に知っていなければならないことになる(期待値をコードで計算するのではなく)。これはテストを作成するときに検討しなければならない主な要因の1つ。今回の場合は簡単で、新しい口座を開設したとすれば、この口座の開始残高は0。200ドル入金したら、0+200=200ドル。


テストコードが完成したら、何らかのテストツールを使って実行させる。例えば、Accountクラスが以下のような最低限の実装だと、当然テストは失敗する。

public class Account {

    public void AddTransaction(decimal amount) {

    }

    public decimal Balance {
        get;
        private set;
    }
}

テスト駆動開発

ユニットテストを実装するにあたって、SUTを完全に実装された状態にする必要はない。テスト駆動開発(TDD)では、ユニットテストを作成する前にSUTを動く状態にしないことが優先される。TDDアプローチを使ってソフトウェアを作成する際には、ユニットテストとプロダクションコードを交互に書く。そして、プロダクションコードの全てのクラスで、各メソッドが実行すると期待される振る舞いごとに失敗するテストを書く。テストが成功するようになるのは、テストの要件を満たす最も単純な方法でプロダクションコードが実行された後である。
プロダクションコードを修正してテストコードが動くようになったからといって、それで正しいとは限らない。正しい実装でないことを証明するには、要件に基づく新しいテストを追加すれば良い。新しいテストを追加するたびに、SUTの実装上の選択肢はさらに制限されることになる。各テストは振る舞いの期待値をもたらし、各期待値はSUTのバランス配分を要求する。
全てのテストを成功させることができたら、少なくとも現時点では、AddTransactionメソッドの100%正しい振る舞いが明らかになる。要するに、要件が変化して新しい機能が追加された場合に備えて、クラスの期待値を明文化し、既存のユニットテストにフォールバックできるようにしておく必要がある。これはセーフティネットになる。このようにしておかないと、コードを手動でテストするような狭い状況ではうまく行くように思える変更を、コードに気軽に追加するようになってしまう。だがそうしたコードは、通常とは異なる入力が渡されると動かなくなったり、表面上は無関係な部分を破壊したりする。
今までユニットテストを書いたことがないなら、わざわざ遠回りをしているように思えるかもしれないが、テストを先に書いておくと、ソリューションのオーバーエンジニアリングを回避するのに役立つ。そして、既存の機能が動かなくなるのを防ぐ回帰テストが提供されるようになる。

より複雑なテストについて

長くなるので詳述しないが、本書ではより複雑なクラスのテストコードを完成させていく一例についての記述もある。SUTであるクラスが別クラスに依存している場合、別クラスの動作内容を定義するためにMockを導入したりする必要があるので、Arrangeの部分が複雑になる。また、別クラスのインスタンスがnull参照の場合はどうなるか、メソッドが例外をスローした場合はどうなるかということも考えないといけなくなるので、テストメソッドの数も増える。テストコードとプロダクトコードを交互に書いていくことや、AAAパターンに従うことは変わらない(Arrange,Act,Assertの全てが記述されるとは限らない。Arrange,Actのみで、Assertがない場合もあり得る)。

テストのBuilderパターン

テストが手に負えなくなる理由は様々で、ほとんどの場合は、テストの対象となるコードが特にテストしやすいものではないため。このような場合、テストは問題のまわりをぐるぐる回りながら、何とかしてコードをテストしようとします。しかし、非常にテストしやすいコードであっても、明瞭さや簡潔さに欠けるテストになってしまうことがある。
オブジェクトを様々な側面から複数の方法で構成することが可能である場合は、Builderパターンを適用すると、オブジェクトを生成するコードが単純になり、コードの意図が明確になる可能性がある。
以下のテストコードにBuilderパターンを適用してみる。このコードはArrangeセクションにおいてコードの意図が明確でなく、このコードが何をするのかを理解するには、各行を注意深く読む必要がある(ここでは、モックフレームワークとしてMoqを使用しているが、本質的ではない)。

[TestMethod]
public void AddingTransactionToAccountDelegatesToAccountInstance() {
    // Arrange
    var account = new Mock<Account>();
    account.Setup(a => a.AddTransaciton(200m)).Verifiable();
    var mockRepository = new Mock<IAccountRepository>();
    mockRepository.Setup(r => r.GetByName("Trading Account"))
                  .Returns(account.Object);
    var sut = new AccountService(mockRepository.Object);

    // Act
    sut.AddTransactionToAccount("Trading Account", 200m);

    // Assert
    account.Verify();
}

以下、BuilderパターンをSUTであるAccountServiceの生成に使用する。

public class AccountServiceBuilder {
    private readonly AccountService _accountService;
    private readonly Mock<IAccountRepository> _mockAccountRepo;

    public Mock<Account> MockAccount {
        get;
        private set;
    }

    public AccountServiceBuilder() {
        _mockAccountRepo = new Mock<IAccountRepository>();
        _accountService = new AccountService(_mockAccountRepo.Object);
    }

    public AccountServiceBuilder WithAccountCalled(string accountName) {
        MockAccount = new Mock<Account>();
        _mockAccountRepo.Setup(r => r.GetByName("Trading Account"))
                        .Returns(MockAccount.Object);

        return this;
    }

    public AccountServiceBuilder AddTransactionOfValue(decimal transactionValue) {
        MockAccount.Setup(a => a.AddTransaction(200m)).Verifiable();
        return this;
    }

    public AccountService Build() {
        return _accountService;
    }
}

AccountServiceBuilderをユニットテストに組み込むと、以下のコードが得られる。Arrangeセクションの意図が明確になっている。

[TestClass]
public class AccountServiceTests {

    private AccountServiceBuilder _accountServiceBuilder;

    [TestInitialize]
    public void TestInitialize() {
        _accountServiceBuilder = new AccountServiceBuilder();
    }

    [TestMethod]
    public void AddingTransactionToAccountDelegatesToAccountInstance() {

        // Arrange
        var sut = _accountServiceBuilder
            .WithAccountCalled("Trading Account")
            .AddTransactionOfValue(200m)
            .Build();

        // Act
        sut.AddTransactionToAccount("Trading Account", 200m);

        // Assert
        _accountServiceBuilder.MockAccount.Verify();
    }
}

Builderパターンには、より宣言型のテストにつながる、という利点もある。どのテストでも循環的複雑度が1を越えることがないようにすべき。つまり、テストは完全に線形でなければならず、分岐があってはならない。if文、forループ、foreachループ、またはwhileループを使用すると、テストの循環的複雑度が1を超えてしまう。Builderパターンは、命令構文と初期化を宣言型に変換することで、循環的複雑度を抑えるのに役立つ。

まとめ

あなたが作成するユニットテストはそれぞれ、コードの期待値を表すものでなければならない。コードと同様に、ユニットテストは技術的な成果物だが、オブジェクトが現実の概念をカプセル化するのと同様に、ユニットテストはオブジェクトに現実の振る舞いを適用する。
テストファーストアプローチをこつこつ続けているうちに、失敗するユニットテストを作成してからでなければ新しいプロダクションコードを記述しなくなる。そして、失敗するユニットテストを成功させるのに十分な、最も単純なプロダクションコードを記述するようになる。当然の結果として、プロダクションコードは自然にユニットテストの期待値を満たすものになる。


  1. 監訳者の方は、本書の読者対象は、初学者からベテランまでのC#開発者すべてだと断言している。また、サンプルコードはC#でも、他のプログラミング言語(特に型付けされた言語)で開発しているチームにとっても大切なエッセンスを学べるとも。 

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1