DeNA Testing Blog

Make Testing Fun, Smart, and Delighting End-Users

SWETの新メンバーから見て驚いたこと、そこから生まれたDIライブラリ不使用宣言

はじめまして!4/1よりSWETに加わった@Kuniwakです。 今回は、私がSWETに入って驚いたことと、そしてSWETだからこそ生まれたものについてお話しします。

まずKuniwakはどんな人?

開発を高速化させるテストや静的検査を生業としています。主に、以下のような記事やスライドを書いています。

では、こんな私がSWETという自動テストのエキスパートのチームで働くことになって驚いたことを紹介します。

SWETに入って驚いたこと

SWETに入って驚いたのは、テストに関連するトピックについてどなたも一家言をもっていたことです。例えば、以下のようなやりとりが実際にありました。

Kuniwak:Mockライブラリ1やDIライブラリ2は必要だと思いますか?いずれもライブラリに頼らずにシンプルに書ける気がします。皆さんはどういうご意見をお持ちでしょうか。

A さん:Mockはライブラリに頼りたいですね。自前でMockのコード書くとMock自体のテストとかが必要になって面倒なことになると思います。

B さん:複数のプログラミング言語にまたがるような組織では、それぞれの言語に対するメンバーの習熟度にもばらつきがありますから、既存の文献やコード資産を参考にできるデファクトなライブラリを使った方がいいという視点もありますね。

C さん:Mockライブラリに限らず、他のライブラリと同じようにメンバーの学習コストやメンテナンスの継続性、利用者数などから考慮するといいのではないでしょうか。

D さん:この記事3でも言われているように、Mockライブラリがパワフルになりすぎて濫用しがち、という話はありますね。個人的な経験で言えば、テストではStub/Spy/Fake4で十分だと思っていて、あまり無理にライブラリは使わない感じですね。

このやりとりでは、会話しているメンバーのいずれもMock/DIライブラリについての深い経験をもっていることに感動しました。また、だからこそそれぞれが違った意見をもっているのだと感じます。このような議論のできる環境は、自動テストのエキスパートで構成されるSWETでなければなかなか巡り会えないのではないでしょうか。

さらに、このような素晴らしい議論ができたことで、以下のとても重要な知識を得られました。

  • Mock/DIライブラリの不必要派は1人じゃない

    今まで、Mock/DIはライブラリを使うのが当たり前というのがよく聞く意見で、私のようにライブラリを不必要と思う派閥はいないのかもしれないと思っていました。しかし、Dさんのように私と同じライブラリの不必要派は一定数存在することがわかりました。

  • Mock/DIライブラリの必要性の議論では無条件の合意をとれない

    ここに書ききれなかったやりとりの中で、Mock/DIライブラリを必要だと思うかどうかは経験してきた言語/プロジェクトの性質によって左右されることがわかりました。例えば、これまで私はJavaScriptやSwiftといった変化の著しい言語を同時に複数を相手にしてきたため、ライブラリの恩恵よりも負の側面を強く感じていたことに気づきました。逆に、このような環境でなければライブラリの恩恵の側面を強く感じたとしても不思議ではありません。つまり、Mock/DIライブラリを使うべきかどうかは言語やプロジェクトに依存するのです。

  • Mock/DIライブラリを使わない選択肢を解説する文献が少ない

    これはBさんとCさんの意見からうかがえる事実です。このやりとりをするまで、文献の少なさを気にしたことがありませんでした。

このやりとりのあと、私はすぐにここで得た知識を活かす機会を思いつきました。私が声をあげることで、本来ライブラリを使わなくてもよい状況でライブラリを無理に使ってしまう問題を減らせると思ったのです。そこで、「バニラDI宣言(Vanilla DI Manifesto)」と「バニラMock宣言」を書くことにしました。

メンバーとの議論から生まれた「バニラDI宣言」と「バニラMock宣言」

バニラDI/Mock宣言とは、状況に応じてライブラリを使わないことを選ぼう、ということを表明した宣言です。このバニラという表現は、何も手が加えられていないという意味でよく使われるものです。ライブラリを使わずに言語本来の自然な書き方で実現していくという方針にぴったりだと思い、この名前をつけました。なお、以降では既に公開済みのバニラDI宣言の方を中心に解説していきます(バニラMock宣言は鋭意準備中です)。

さて、そもそもライブラリを使わないでDIを実現できるのでしょうか?この疑問にお答えするために、バニラDI宣言には各言語のコードサンプルを付属させました。例えば、JavaScriptにおけるバニラDIは次のように表現できます:

// これらは依存先コンポーネントです。後述のコンポーネント内で使われます。
class X {}
class Y {}

// これは依存元コンポーネントです。X と Y への依存をもっています。
class Z {
  constructor(dependency) {
    this.dependency = dependency;
  }

  doSomething() {
    const {x, y} = this.dependency;
    // x と y を使って処理をします。
  }
}

// すべての依存先コンポーネントは、依存元コンポーネントの
// 初期化時に束縛します。
const z = new Z({
  x: new X(),
  y: new Y(),
});

z.doSomething();

ご覧の通り、DIをコンストラクタ引数への指定だけで実現しています(この方法はコンストラクタ注入と呼ばれます)。とても単純な仕組みであることがよくわかると思います。また、静的型検査のある言語でもどのように表現されるのか見てみましょう。例えば、Swiftでは次のようになります:

// これらは依存先コンポーネントです。後述のコンポーネント内で使われます。
class X {}
class Y {}


// これは依存元コンポーネントです。Xと Yへの依存をもっています。
class Z {
    typealias Dependency = (
        x: X,
        y: Y
    )
    private let dependency: Dependency


    init(dependency: Dependency) {
        self.dependency = dependency;
    }


    func doSomething() {
        let (x, y) = self.dependency;
        // x と y を使って処理をします。
    }
}


// すべての依存先コンポーネントは、依存元コンポーネントの
// 初期化時に束縛します。
let z = Z(dependency: (x: X(), y: Y()));

z.doSomething();

静的型検査のある言語でも同様の書き方で実現できることがわかります。 さて、どうして私はこのようなバニラDIを好むのでしょうか。その理由は以下の6点です:

  • 実装が単純
  • 簡単に利用できる
  • 依存するライブラリはゼロ
  • 初心者に対しても可読性が高い
  • 多くの言語で実用可能
  • 悪い設計だとすぐに破綻する

このうち上の5つは自明だと思いますが、私が特に強調したいのは最後の「悪い設計だとすぐに破綻する」ということです。では、破綻のスメルが出ている具体例を見てみましょう。

バニラDIが可視化する設計の破綻

まず、コンストラクタの引数が多くなってきたケースです。たとえば次のようなクラスがあったとします:

class SomethingGreatService {
    private let foo: Foo
    private let bar: Bar
    private let fooBar: FooBar
    private let baz: Baz
    private let qux: Qux
    private let quux: Quux


    init(foo: Foo, bar: Bar, fooBar: FooBar, baz: Baz, qux: Qux, quux: Quux) {
        self.foo = foo
        self.bar = bar
        self.fooBar = fooBar
        self.baz = baz
        self.qux = qux
        self.quux = quux
    }
}

クラス定義もだいぶつらそうですが、それよりつらいのはコンストラクタの呼び出し側です。たとえば以下のようになるかもしれません:

let service = SomethingGreatService(
    foo: Foo(),
    bar: Bar(
        corge: Corge()
    ),
    fooBar: FooBar(
        grault: Grault(
            garply: Garply()
        )
    ),
    baz: Baz(
        waldo: Waldo()
    ),
    qux: Qux(),
    quux: Quux(
        plugh: Plugh()
    )
)

特にSomethingGreatServiceを対象としたユニットテストでは繰り返しこのクラスを作成することになることが多く、テストを書いている途中でうんざりすることになります5。これに対して、DIライブラリを使えば呼び出し側の記述をかなり省略できます。この点だけを見れば、DIライブラリの方が優れているという印象を受けることでしょう。しかし、バニラDIでは異なる見方をします。

バニラDIでは、このような状況を設計破綻のスメルとみなします。今回のスメルは次の2つの可能性のいずれかを示唆しています:

  • SomethingGreatServiceの責務過多
  • 依存対象であるFooBar等の抽象度の不足

そして、それぞれへ対応する解決策は次のようになります:

  • SomethingGreatServiceの責務を分割
  • 依存対象の関係を整理してこれらをまとめた新コンポーネントを作成

さて、これらの解決策によって先ほどの引数が多いという問題も同時に解決できます。前者の責務分割では、SomethingGreatServiceが複数のコンポーネントへ別れますから、それぞれのコンポーネントへコンストラクタ引数が分散されます。後者の依存物の整理では、直接の依存が新コンポーネントを通しての間接的な依存へ変わりますから、直接の依存であるコンストラクタの引数は少なくなります6

このように、バニラDIでは設計の破綻が書きづらさや面倒さとなってすぐに可視化されます。これらを取捨選択して解決していく過程で、自然と適切な責務の分割や抽象化へと導かれるのです。これこそがバニラDIの最大の利点の1つだと私は考えています。

終わりに

この記事では、私から見てSWETに入って驚いたこと、そしてテストに関するエキスパートが集結しているSWETでなければ思いもつかなかったバニラDI/Mock宣言について紹介しました。バニラDI/Mock宣言についてご質問やご意見等ある場合は、お気軽にissueKuniwakまでお問い合わせください。では、弊社主催であるiOS Test Nightの運営スタッフとしてお会いできることを楽しみにしております。


  1. Mockライブラリとは、テストケースの実行に必要な偽のコンポーネントをつくりやすくするライブラリです。JavaScriptではSinon.JS、SwiftではCuckoo、Pythonではunittest.mockが有名です。

  2. DIライブラリとは、Dependency Injectionをやりやすくするライブラリです。JavaScriptではInversifyJS、SwiftではDIKitが有名です。

  3. t-wadaさんと家永さんの対談記事:「希薄化したTDD、プロダクトの成長のために必要なものは?〜『健全なビジネスの継続的成長のためには健全なコードが必要だ』対談(6)

  4. これらはすべてテスト用の偽のオブジェクトのことです。Stubとは、テスト対象のコンポーネントへの入力をテスト用の入力へ差し替えるためのオブジェクトです。Spyはテスト対象のコンポーネントからの出力を記録するためのオブジェクトです。Fakeはテスト対象からの依存対象を簡易的に代替するオブジェクトです。それぞれを詳しく知りたい場合はxUnit Test Patternsを読むことをお勧めします。

  5. このつらさを解消するためにデフォルト引数を使うこともできますが、一般的にこれはあまりよい選択ではありません。デフォルト引数は何がデフォルトとして使われても問題のないケースでのみ使うべきだからです。もし、デフォルト引数を変えたら壊れてしまうような場合は、定義元を読まない限りわからない暗黙の期待を埋め込んでいるということを意味します。このような暗黙の期待は可読性を損ないます。つまり、呼び出し側が特定の振る舞いを期待する場合は明示的に引数に指定する方がよいのです。今回のケースでは何がデフォルトとして使われても構わないとはいえないので、デフォルト引数を使わない方がよいでしょう。

  6. 素直に考えると、新コンポーネントを作成しても依存している数には変わりないと思うかもしれません。しかし、実際はそうなりません。まず、抽象コンポーネントを定義することでStub/Spy等のテストダブルを書きやすくなります。テストダブルは依存物を必要としないですから、テスト内では間接的な依存を意識しなくてすむようになります。また、テスト外の場合でも、SomethingGreatServiceを内部的に利用するクラスが既に作成済みの依存物を保持していることが多く、テスト内と同様に間接的な依存は見えなくなります。もし、間接的な依存が見えてしまう場合はカプセル化が不十分であることを示唆しています。