テスト「書いて」みませんか? - Don't just Do Test, Write Test.

T.Kawasaki

1 はじめに

1.1 自己紹介

1.2 今回の目標

  • 皆さんに「テスト書こう!」と思ってもらえること
    • 業務で
    • 個人プロジェクトで

1.3 話すこと

  • テストを書くことの意義
  • テストの書き方、やり方
  • どうやってテスト書くようにしていくか

1.4 話さないこと

  • 特定の言語・ライブラリに特化した話
    • 例としてJUnitを使いますが、JUnit固有の話はしません

2 なぜテストを書くのか

2.1 テスト書くのめんどくね?

  • 打鍵すりゃいいじゃん
  • 繰り返さないなら無駄じゃね

2.2 テストの粒度を細かくできる

xUnitを使ってユニットテストを書くと、テスト対象の粒度が細かくできます。

テスト手法 テスト対象
xUnit クラス, メソッド
手動テスト 画面, プログラム

組み上がった巨大なシステムより、部品のほうがテストが楽

2.2.1 パチンコ部品検査のバイト

  • 玉が通過したことを感知するセンサー
  • 先端に玉がついた棒を、センサーに通して確認

この部品を、パチンコ台に組み込んでからテストするとしたら…?


2.3 テスト実行が効率的

  • テスト準備がかんたん
  • 結果確認がかんたん
  • 実行が速い

2.3.1 テスト準備、入力がかんたん

  • 仕組みを作れば、あとはデータを入れ替えるだけ
  • 10画面遷移するカード新規入会画面
    • 8画面目でバグを発見し、テストを最初からやりなおし

2.3.2 結果確認がかんたん

  • 値の比較はコンピュータの得意分野
  • その目視確認、本当にちゃんとできていますか?

2.3.3 実行が速い

  • 大きなwar,jarのビルド不要
  • デプロイ不要、サーバ再起動不要
  • 人がやるより速い

テスト→プロダクト→テスト…と細かく作業できます。

2.4 設計品質向上につながる

  • 良い設計であれば、テストがしやすい。
  • テストが書きにくいと感じたら、設計に問題あるかも

2.4.1 『レガシーコード改善ガイド 』

良い設計はテスト可能であり、テスト可能でない設計は悪い設計である、ということは常に真実です。

2.4.2 平鍋 健児さん

EoT(Ease of Testing)の高い設計が、よいオブジェクト指向設計である。

http://blogs.itmedia.co.jp/hiranabe/2005/08/post_e66c.html

2.5 資産になる

  • 回帰テスト
    • FW, ミドルウェア等のバージョンアップに
  • システムの仕様(振る舞い)を表す
    • プロダクションコードと一緒にバージョン管理される
      • (その設計書、最新ですか?)

2.6 新しい技術・手法を取り入れる

テストコードが無いと新しい技術を取り入れられない。

  • リファクタリング
  • アジャイル
  • 継続的インテグレーション, 継続的デリバリー, DevOps
  • ドメイン駆動設計(Domain Driven Design)
  • マイクロサービスアーキテクチャ
  • テスト駆動開発(Test Driven Development)

2.6.1 テストに関連する技術・手法の依存関係


3 逆に、テストを書かないと…

3.1 システム外部からテストするしかなくなる

ブラウザやコマンドラインからしかテストができません。

  • 内部のアーキテクチャ検討が適当になる
  • モジュール単体(クラス、メソッド)の品質を上げる機会を失う
  • エントリポイントにだらだら処理を書く
    • 巨大なexecuteメソッド

3.2 例: SmartUI? (2つの値を加算する)


void 計算クリック() {
   sum = 数1.value + 数2.value;
   結果.value = sum;
}

3.3 何がまずいのか?

  • UIと計算が一体化している。
  • 2つの数値を引数にとる計算クラス/メソッドに分離されていると、計算だけをテストできる

3.4 構造を修正できない

  • テストコード無しでリファクタリングは非現実的
    • リファクタリング - 振る舞いを変えずに内部構造を変える


3.5 開発のツケが保守フェーズの負担となる

  • スパゲッティのため修正(コード理解)に工数がかかる
  • 回帰テストがないのでテスト工数がかさむ
  • 素早く開発→デプロイを繰り返すこともできない
  • 自分たちのやり方に自信が持てず、保守メンバーのモチベーション低下にも

4 テストコードの書きかた超入門編

4.1 JUnitとは

Java用テスティングフレームワーク

http://junit.org/

4.2 テストコード例

public class MyFirstTest {
    // メソッドに'@Test'アノテーションを付与することで、
    // テストメソッドとして認識されます
    @Test
    public void 指定した要素のインデックスを取得できること() {
        // 準備: テストの事前条件を整えます
        // 0:東京, 1:大阪, 2:名古屋
        List<String> cities = Arrays.asList("東京", "大阪", "名古屋");

        // 実行: テスト対象の処理を呼び出します
        int indexOfOsaka = cities.indexOf("大阪");

        // 結果比較: 実行結果が期待通りであることを確認します
        assertEquals(1, indexOfOsaka);
    }
}

4.3 API補足解説(抜粋)

JUnit メソッド 表明する内容
assertEquals(expected, actual) 2つの引数が等価であること
assertTrue(condition) 引数がtrueであること
assertNotNull(object) 引数がnullでないこと

現在は、上記メソッドではなくassertThatが使用されますが、今回はassertEqualsのみ使用します。

4.4 表明(assertion)とは

  • 「こうなっているはずだ!」「こうなっていないとおかしい!」という宣言くらいの意
  • 表明が成立しなかった場合、例外が発生しテストが失敗します

4.5 TDD Bootcampを例に(デモ)


Figure 5: 3から8の閉区間

5 こんなときどうテストするか

5.1 テストしにくいこんな場合

  • 実行時によって動作が変わるケース
    • システムクロック(現在日時)
    • 乱数
  • 引数以外のI/Oが必要なケース
    • ファイルI/O
    • データベース

5.2 基本的な考え方

  • ◎ 依存関係を断ち切る
  • △ テストがカバーする範囲を大きくする
    • 依存関係まとめてテストする

5.3 ケース1: このメソッドは戻り値がなくファイルに結果を出力します

  • 出力先への依存を断ち切る
  • ファイルへ出力する前の値を検証する

コードで説明

5.3.1 解説

修正前


修正後


5.4 ケース2: 乱数を使うので毎回戻り値が変わります

  • 乱数(Random)への依存を断ち切る

コードで説明

5.5 データベースのテストをどうするか

ほとんどのシステムはデータベースを使用するが…

  • DBアクセスまで含めてテストする
  • モックを使用する

5.5.1 参照系のテスト

  • 準備データをDBに登録する
  • テストを実行する
    • SELECTが発行される
  • 結果を比較する


5.5.2 更新系のテスト

  • 準備データをDBに登録する
  • テストを実行する
    • INSERT/UPDATEが発行される
  • DBの値を検証する


5.5.3 DBアクセスを含むテストの問題点

  • すごーーーーーーーーく 遅い
  • データの準備・保守がたいへん
    • 書き方(テストコード, CSV, XML, YAML, Excel…)
    • メンテナンス(スキーマ変更に弱い)
  • 順番依存のテストになるおそれあり
    • トランザクション管理

5.5.4 DBアクセスするテストは「ユニットテスト」か

実行に0.1秒もかかる単体テストは、遅い単体テストである。

『レガシーコード改善ガイド』

瞬時のフィードバックが得られなくなる。

5.5.5 スローテスト対策:DBアクセスをモック化

テスト実行時に部品をすり替えてDBアクセスしないようにする。

  • DAOパターンを使用している場合はモックに差し替える
  • JMockitなどのモックライブラリを使用する

5.5.6 DBアクセスモック化の問題点

  • DBとSQLというシステムの肝心要がテストコードでカバーできない
  • SELECT結果を写し取る悲しさ

5.5.7 どうするか?

  • DB,SQLのテストを書きたければ、やるしかない
    • ○ SQLやストアドが重要なシステム
    • ✕ DBを永続化のために使用しているシステム
  • 参照系だけテストを書く
    • 複雑なクエリはSELECTに集中している
  • DBアクセスする遅いテストとそうでない速いテストを分ける
    • 速いテスト→遅いテストの順に実行する

6 新規プロジェクトにテストを導入する

6.1 やっておくべきこと

  • 最初から計画に織り込んでおくこと
  • テストコードのサンプルを作成すること
  • テストコードを正式な成果物として定義すること
  • テストを自動実行できるようにすること(CI)

6.2 計画

  • あらかじめコスト、期間を積んでおく
  • どうテストを書く/するのか検討する計画にしておく

6.2.1 テスト戦略を立てる

各ステレオタイプをどうテストするか検討をしておく。

  • どのクラスを
  • どの工程で
  • どうやって

テストするか

6.2.2 ステレオタイプ毎のテスト例

クラス 自動 手動 説明
Action - 画面との結び付きが強い
Form ユニットテストが容易
Service ロジックの中心。がんばる
Entity 自動生成
View - レイアウト確認が必要
Utility ユニットテストが容易
  • ○ このテストで品質担保する
  • △ 他のテストで間接的にテストする

6.2.3 全てのテストを書こうとしない

  • 最初はテストを書く範囲は狭くてもよい。
    • 0->1の進歩は極めて大きい。
  • 手動でテストする範囲は残るものと考える。
    • 少なくとも画面レイアウトは人間が見ることに(2017年現在)。

6.2.4 ハマりポイント(E2Eテスト)

いきなりEnd to Endのテストは難易度高い

  • 作成も修正も大変
  • Seleniumはブラウザ自動化ツール(≠テストツール)

6.2.5 ハマりポイント(カバレッジ)

カバレッジを終了条件にするのは諸刃の剣

  • カバレッジは命令/分岐を通過したかどうかしか示さない
  • カバレッジを100%にするため非効率な努力をすることも
    • プライベートコンストラクタ
    • if (logger.isDebugEnabled()) {
    • Viewからしか呼ばれないアクセサメソッド

6.3 サンプルを作成する(重要!)

  • サンプルが整備できていれば、ほとんどの開発者はテストを書ける。
    • パイロット開発時にテストコードも整備する。
  • ここで手を抜いて開発者/委託先に丸投げすると、コードにバラツキが出る。
    • メンテされなくなる。

6.4 成果物定義

テストコードは成果物です。 レビュー しましょう。

  • 可読性、保守性
  • テスト実行順序に依存していないか
  • ケースが多すぎないか

6.5 CI戦略を立てる

頻繁にテストを全実行しなければ、簡単にテストは壊れます。

  • テスト失敗を検知した場合、速やかに(他のタスクを止めて)修正する。
    • 対応が早いほど修正コストは低くなります。
  • 朝会で毎日テスト結果確認
    • 「お見合い」を防ぐ
    • テスト失敗があれば、その場で誰が対応するかを決めましょう。

7 既存プロジェクトにテストを導入する

7.1 既存プロジェクト特有の難しさ

  • テストを後付けで整備する 工数 が捻出できない
  • 仕様 がわからない
  • 「レガシーシステム」の ジレンマ
    • 構造が複雑すぎてテストが書けない
    • テストが書けないので構造を変えられない

どう立ち向かっていくか。

7.2 テストコードを整備する余力がありません

  • 私のシステムには テストコードがありません
  • テストコードを整備する 工数を捻出できません

7.2.1 突破口

  • システム全体にテストコードを整備するのは大変
  • 追加/変更する箇所だけ テストを書く

以下、具体的なテクニックを『レガシーコード改善ガイド』より。


7.2.2 スプラウトメソッド

既存の巨大メソッドに機能追加する必要がある

  • 追加機能を新規メソッドで作成する
  • そのメソッドのテストを書く
  • 元の巨大メソッドから新規メソッドを呼び出す
void 既存メソッド(String input) {
     // 既存の処理
     //     :
     新規メソッド(input);   // 呼び出し
     //     :
     // 既存の処理
}
void 新規メソッド(String input) {
     // 追加機能
}

7.2.3 スプラウト(sprout)のイメージ


7.2.4 例:修正前コード

public void register(Member member) {
    // DBに登録
    db.insert(member);
}
  • 登録する前にバリデーションを入れたい。

7.2.5 そのまま処理追加した例

public void register(Member member) {
    // 入力項目のバリデーション
    if (member.getName().isEmpty()) {
         throw new IllegalArgumentException("名前が入力されていません");
    }
    if (member.getPostalCode().matches("[0-9]{7}")) {
         throw new IllegalArgumentException("郵便番号が不正です");
    }
    // その他のバリデーション .....

    // DBに登録
    db.insert(member);
}

7.2.6 問題点

  • 正しく変更できたかどうか確認できない
    • 既存機能にテストは無く、既存メソッドのテストを書くのは大変
  • 「バリデーション」と「DB登録」という 2つの操作 がゴチャっと混在している

7.2.7 解決案

public void register(Member member) {
    // 入力項目のバリデーション(『スプラウトメソッド』の呼び出し)
    validate(member);

    // DBに登録
    db.insert(member);
}

//// 新しく追加された『スプラウトメソッド』
//// ここは簡単にテストを書ける!
void validate(Member member) {
    if (member.getName().isEmpty()) {
         throw new IllegalArgumentException("名前が入力されていません");
    }
    if (member.getPostalCode().matches("[0-9]{7}")) {
         throw new IllegalArgumentException("郵便番号が不正です");
    }
    // その他のバリデーション .....
}

7.2.8 そのテストコード

@Test
public void 郵便番号の桁数が不足している場合_例外が発生すること() {
     MemberService service = new MemberService();
     Member member = new Member();
     member.setName("山田太郎");
     member.setPostalCode("123456");  // 郵便番号6桁なのでエラー
     try {
         service.register(member);
         fail("期待した例外が発生しませんでした");
     } catch (IllegalArgumentException e) {
         assertEquals("郵便番号が不正です", e.getMessage());
     }
}

※既存機能のDB登録は全く確認していない。

7.2.9 スプラウトクラス

スプラウトメソッドのクラス版

  • 追加する機能を実現するクラスを新規追加する
  • そのクラスのテストを書く
  • 元の巨大メソッドから新規クラスを呼び出す


7.2.10 スプラウトクラスの例

public void register(Member member) {
    // 入力項目のバリデーション(『スプラウトクラス』の呼び出し)
    MemberValidator validator = new MemberValidator();
    validator.validate(member);
    // DBに登録
    db.insert(member);
}
class MemberValidator {
     void validate(Member member) {
         if (member.getName().isEmpty()) {
              throw new IllegalArgumentException("名前が入力されていません");
         }
         if (member.getPostalCode().matches("[0-9]{7}")) {
              throw new IllegalArgumentException("郵便番号が不正です");
         }
         // .....
     }
}

7.2.11 スプラウトメソッドとの使い分け

以下のケースでは、スプラウトクラスを使用

  • 既存クラスが大きすぎてメソッド追加したくない
  • クラスのテストが書きにくい
    • いろんなものが無いとnewできない、実行できない
      • DBコネクション
      • 入力ファイル
  • 追加する機能が元クラスの責務から外れる

7.3 システムの仕様がわかりません

  • テストを書くには 仕様を理解 する必要があります
  • 設計書はメンテされておらず、何が正解なのか誰もわかりません
  • 唯一言えるのは、 現在の動作が正解 ということだけです

7.3.1 仕様化テスト

現在のシステムの動作を正として、システムの振る舞いを写し取るテスト。
(Characterization Test)

Characterization
性格描写、特徴付け、特性評価

7.3.2 仕様化テストの書き方

  • とりあえず適当な入力で既存機能を呼び出してみる
  • 適当な値で結果比較する
  • 結果比較に失敗するので、期待値を実際の値に書き換える
  • カバレッジを見ながら、上記作業を繰り返す

以下、実際にやってみます。

7.3.3 デモ(例:割引率を計算する。)

  • 1千円以上1万円未満買い上げの方は1割引
  • 1万円以上お買い上げの方は2割引
  • 1千円未満は割引なし
お買上げ額 割引率
~1000円 0%
1000~10000円 10%
10000円~ 20%

「価格に負数が渡されて請求額が不正になるバグが発覚した」とする。

7.4 やる気が出ません

  • 毎日スパゲッティコードを相手しています。
  • とても改善ができると思えません。

⇒レガシーコード改善ガイド 第24章
『もうウンザリです。何も改善できません』

7.4.1 少々コードが整備されたところで現実は変わらないのでは?

「どうせ90パーセントの時間をヘドロのようなものを相手に過ごすのに、この小さい部分だけきれいにしたところで何だというのだ。もちろん、この小さな部分は改善できる。しかし、それが今日の午後、あるいは明日の私にとって何の役に立つだろうか?」。

『レガシーコード改善ガイド』

7.4.2 改善を続けると…

しかし、一貫してそのような小さな改善を続ければ、数ヶ月の間にシステムは見違えるような状態になります。ある日の朝、ヘドロを相手に汚い仕事をするつもりで会社に来ると、次のことに気づきます。

『レガシーコード改善ガイド』

7.4.3 自分が変わる

「あれ?このコードはいい感じになっているぞ。誰かがこのコードを最近リファクタリングしたようだ」。その時点、すなわち優れたコードと悪いコードの違いを直感的に理解できた時こそが、皆さんが変わる時です。

『レガシーコード改善ガイド』

7.4.4 隣の新規開発の芝は、実はそれほど青くない

レガシーシステムを担当する人たちは、しばしば新規開発に携わることを望みます。
<中略>
しかし率直に言って、新規開発には、新規開発なりの問題があります。

『レガシーコード改善ガイド』

7.4.5 取り組む姿勢が重要

私は、数百万行ものレガシーコードを扱っているいくつかのチームを訪れたことがあります。これらのチームは、毎日が挑戦であり、物事をより良くする機会であるととらえ、仕事を楽しんでいました。

『レガシーコード改善ガイド』

新規か保守かは重要でない

7.4.6 結論

  • コードを変えれば自分が変わる(成長する)
    • 正しいやり方 を覚える
      • テストが書ける
      • リファクタリングができる
    • 正しいやり方ができると 自信 が持てる
  • 新規か保守かは重要でない
    • むしろ重要なのは取り組む姿勢

8 おわりに

導入に向けて…

  • 皆さんが一人一人がテスト書けるようになればなるほど、PJにテストコードが導入しやすくなります。
  • 指示をもらうのではなく、 許可・裁量 をもらいましょう

一緒にがんばりましょう!

8.1 おまけ:参考文献

8.1.1 レガシーコード改善ガイド

  • テストコードが無いシステムにどう立ち向かうか


8.1.2 新装版 リファクタリング―既存のコードを安全に改善する―

  • 初版より翻訳が良くなったそうです


8.1.3 JUnit実践入門 ~ 体系的に学ぶユニットテストの技法

  • Java使いでJUnit初心者ならお勧め


8.1.4 実践 JUnit ―達人プログラマーのユニットテスト技法

  • もうちょっと抽象的
  • 大阪府立図書館にあります