0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

その前に

  • 結局、「ユニットテストを意識してコードを書かないと、ユニットテストしにくい」という事です。
  • 適切な書籍はたくさん存在しますので、具体的な方法はそちらを参考にしてもらったほうがよいです。ここで書いているのは備忘録に近く、なおかつ、特定の言語に依存しないような抽象的な書き方に徹していますので、分かりにくいです。
  • 関数・メソッドを以下「メソッド」と表記します。
  • 備忘録なので頻繁に改訂します(たぶん)。

テストコードが書きにくくなる要因

初めてテストコードを書く時、いざ、やってみようとすると、テストコードが書きにくい、もしくは、書けないことに気付きます。それは何故でしょう?

  1. メソッドの行数が多い、または、複雑
  2. 外部アクセス処理と内部処理の同居
  3. 時刻判定と対応する処理の同居
  4. 外部ライブラリ依存

予めこれらを意識しておく必要があります。

1. メソッドの行数が多い、または、複雑

メソッドの行数が多いのと複雑なのはほぼ同時に発生します。ここで言う「複雑」とは、条件分岐が多いことを意味しています。条件分岐が多い場合、テストコードも大量に書かなければなりません。

このような場合、以下の対策を行います。

  • メソッドを分割する
  • 引数のチェックを早期に行い、早期リターンする
  • 複雑な条件の場合、その条件判定を別メソッドにする

行数や複雑さ(「複雑度」と呼ばれます)は測定するツールがあるため、使用しましょう。
こういう測定を「コードメトリクス」と呼びますが、それをキーワードとして調べると何かしら分かります。

個人的には以下の基準を使います(根拠はありません。ただ、どこかしらで割り切っていないといけないので決めています)。

  • メソッドの行数: コメントや空行を除いて20行以下
  • 複雑度: mccabe複雑度で10以下

この対策を阻害しそうなものとして、以下の事が考えられます。

(1) コーディング規約に「returnは一箇所とする」が存在する
(2) メソッドを分けると性能が低下する

阻害要因(1) コーディング規約に「returnは一箇所とする」が存在する

個人的にはこの規約は嫌いというか、存在する意義を見いだせないのですが、
メソッドを短くすれば、returnを一箇所にしてもよいかと思います。

阻害要因(2) メソッドを分けると性能が低下する

これは確かにその通りですが、多くの場合、そこが問題になることは少なく、
行数が長いメソッドによって、可読性や保守性が低下するほうに問題があります。
言語によっては、小さいメソッドは「インライン化」(メソッドを呼び出し元のメソッドに組み込む)することにより、性能を維持するような仕組みがあります。

2. 外部アクセス処理と内部処理の同居

※ 便宜上、コードとテストコードを同じ箇所に書いていますが、実際は別ファイルにします。

外部リソース、例えば、ファイル、データベース、ネットワークの読み書き処理(接続・切断も含む)がある場合、外部リソースを準備しないとテストコードを実行できません。そこに内部だけで済む処理がメソッドで同居していると、内部処理のテストが面倒になります。

例えば以下のようなファイルを呼んで解析する処理(解析データリストは解析したデータの集合と思ってください。どこかに格納して使いますが、ここでは便宜的にローカル変数においています)

メソッド(ファイル名) {
    ファイル識別子 = ファイルオープン(ファイル名)
    if (オープンできない) {
        return 異常終了コード
    }
    解析データリスト = 空リスト
    while (ファイル読み込みがまだできる) {
        行文字列 = 行読み込み(ファイル識別子)
        行解析し解析データリストに追加
    }
    ファイルクローズ(ファイル識別子)
    return 正常終了コード
}

のようなメソッドがあると、ファイルのオープン〜クローズまでエラーを含めてテストコードを書く必要があります。可能なら以下のようにしたいです。

メソッドA(ファイル名) {
    ファイル識別子 = ファイルオープン(ファイル名)
    if (オープンできない) {
        return 異常終了コード
    }
    解析したデータリスト = メソッドB(ファイル識別子)
    ファイルクローズ(ファイル識別子)
    return 正常終了コード
}

メソッドB(ファイル識別子) {
    解析データリスト = 空リスト
    while (ファイル読み込みがまだできる) {
        行文字列 = 行読み込み(ファイル識別子)
        解析データ = メソッドC(行)
        解析データリストに追加
    }
}

メソッドC(行文字列) {
    行解析
    return 解析データ
}

というのも、行を解析する処理は大抵非常に長く複雑なので、詳細にテストしたいものです。しかし、ファイルアクセスと同居してしまうと、色々なテストデータを含むファイルを用意せねばならないので、テストが大変になります。

似たような話で、外部変数に依存するコードも、引数で渡すようにすると、テストコードが書きやすいです。

外部変数E1
外部変数E2

メソッドA() {
    外部変数E1を参照する処理
    外部変数E2を参照する処理
}

// 以下テストコード

テストメソッドA() {
    外部変数E1 = テスト用データ1_1
    外部変数E2 = テスト用データ1_2
    assert(期待値, メソッドA())

    外部変数E1 = テスト用データ2_1
    外部変数E2 = テスト用データ2_2
    assert(期待値, メソッドA())

    ...
}

これを

メソッドA() {
    メソッドB(外部変数E1, 外部変数E2)
}

メソッドB(E1, E2) {
    E1, E2を参照する処理
}

// 以下テストコード

テストメソッドA() {
    // メソッドAに対するテストは1項目だけでよい
    外部変数E1 = テスト用データ1_1
    外部変数E2 = テスト用データ1_2
    assert(期待値, メソッドA())
}

テストメソッドB() {
    assert(期待値, メソッドB(テスト用データ1_1, テスト用データ1_2))
    assert(期待値, メソッドB(テスト用データ2_1, テスト用データ2_2))
}

3. 時刻判定と対応処理の同居

外部リソースと似たような話ですが、時刻を取得するAPIが呼ばれ条件判定する処理と、それに対応する処理を同居させていると、テストコードの実行に非常に困難が伴います。例えば、4時になったら行う処理があるとして、

メソッドA() {
    if (現在時刻が04:00:00を過ぎた場合でその日は未実行の場合) {
        その処理
    }
}

とすると、実際に4時になるまで待つか、実行マシンの時刻を無理やり変更しないと、「その処理」が実行できないことになります。もちろんそういう検証はどこかでしないといけないわけですが、「その処理」を細かく検証するのには向きません。ちなみにここでは固定で「04:00:00」とおいているのは違う意味で問題ですが、そこはおいて置きます。

ここでの対策は2個あります。

  • 時刻取得API(クラス)を抽象化して引数で渡す
  • 時刻取得をラッパーにして、テスト時と実働時で挙動を変える

時刻取得APIを抽象化した場合

メソッドA(時刻取得API) {
   現在時刻 = 時刻取得API()
   if (現在時刻が04:00:00を過ぎた場合でその日は未実行の場合) {
       その処理
   }
}

// メソッドAを呼び出すメソッド
メソッドB() {
    メソッドA(実際に時刻を取得するもの)
}

// メソッドAのテストコード
テストメソッドA() {
    ダミー時刻に 03:59:59 をセット
    assert(期待値, メソッドA(ダミーの時刻を取得するもの))
    ダミー時刻に 04:00:00 をセット
    assert(期待値, メソッドA(ダミーの時刻を取得するもの))
}

実際に時刻を取得する() {
    return 実際の時刻を返す
}

ダミー時刻を取得する() {
    return ダミー時刻を返す
}

この場合、メソッドBのテストで実際の時刻をどうテストするかで悩むことになります。
次に、時刻取得をラッパーを作る場合

ダミー時刻を取得するかの外部変数(初期値はFalse)
ダミー時刻の外部変数

時刻取得ラッパー() {
    if (ダミー時刻を取得するか) {
        return ダミー時刻
    } else {
        return 実際の時刻
    }
}

メソッドA(時刻取得API) {
   現在時刻 = 時刻取得ラッパー()
   if (現在時刻が04:00:00を過ぎた場合でその日は未実行の場合) {
       その処理
   }
}

テストメソッドA() {
    ダミー時刻を取得するか → True

    ダミー時刻に 03:59:59 をセット
    assert(期待値, メソッドA())
    ダミー時刻に 04:00:00 をセット
    assert(期待値, メソッドA())

    ダミー時刻を取得するか → False(初期値)
}

時刻もある意味「外部」の話となるため、取得と切り分けることでテスタビリティの向上を図ることができます。

4. 外部ライブラリへの依存

外部ライブラリに依存するクラスやメソッドがある場合、すべて存在しないと実行できません。依存先が多くなるとテストコードを書きにくくなるというより、テスト実行に支障が出ることになります。

メソッド() {
    外部ライブラリAのメソッドを呼ぶ
    外部ライブラリBのメソッドを呼ぶ
    外部ライブラリCのメソッドを呼ぶ
    外部ライブラリDのメソッドを呼ぶ
}

こういった場合、可能なら依存先を減らすようなリファクタリングを行います。

// ファイル1
// このファイルは外部ライブラリAに依存するものにする
メソッドA1() {
    外部ライブラリAのメソッドを呼ぶ
}

メソッドA1() {
    外部ライブラリAのメソッドを呼ぶ
}
// ファイル2
// このファイルは外部ライブラリBに依存するものにする
(以下略)

このようにメソッドを分割するとともに、ファイルも別にすることになります。単一の依存先にすることは通常不可能なので、複数の依存先になるのは仕方ありませんが、1ファイル、1メソッドの依存先を「減らして」いくことが重要です。

ただ、これはかなり難しい作業になるので、途中からこれをやると確実に時間がかかります。
はじめから意識して書く必要があります

例えば、Androidアプリで、計算ロジックとかは Android SDK に依存せず、純粋な Java だけで書ける部分が出てくるかと思います。そうすると、そのクラスだけ mockito などを「使わないで」テストできるようになり、テスト実行時間も短縮できたりします(エミュレータを起動しなくてよいので)。
ちなみに、そこコードを他に再利用しやすくなるという副作用もあります。

まとめ

内部で済む処理(メモリ上だけで済むような処理)は、それだけをまとめるとテストがしやすいです。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?