xUnitを使ってユニットテストを書くと、テスト対象の粒度が細かくできます。
テスト手法 | テスト対象 |
---|---|
xUnit | クラス, メソッド |
手動テスト | 画面, プログラム |
組み上がった巨大なシステムより、部品のほうがテストが楽
この部品を、パチンコ台に組み込んでからテストするとしたら…?
テスト→プロダクト→テスト…と細かく作業できます。
良い設計はテスト可能であり、テスト可能でない設計は悪い設計である、ということは常に真実です。
EoT(Ease of Testing)の高い設計が、よいオブジェクト指向設計である。
テストコードが無いと新しい技術を取り入れられない。
ブラウザやコマンドラインからしかテストができません。
void 計算クリック() { sum = 数1.value + 数2.value; 結果.value = sum; }
Java用テスティングフレームワーク
public class MyFirstTest { // メソッドに'@Test'アノテーションを付与することで、 // テストメソッドとして認識されます @Test public void 指定した要素のインデックスを取得できること() { // 準備: テストの事前条件を整えます // 0:東京, 1:大阪, 2:名古屋 List<String> cities = Arrays.asList("東京", "大阪", "名古屋"); // 実行: テスト対象の処理を呼び出します int indexOfOsaka = cities.indexOf("大阪"); // 結果比較: 実行結果が期待通りであることを確認します assertEquals(1, indexOfOsaka); } }
JUnit メソッド | 表明する内容 |
---|---|
assertEquals(expected, actual) | 2つの引数が等価であること |
assertTrue(condition) | 引数がtrueであること |
assertNotNull(object) | 引数がnullでないこと |
現在は、上記メソッドではなくassertThatが使用されますが、今回はassertEqualsのみ使用します。
Figure 5: 3から8の閉区間
コードで説明
修正前
修正後
コードで説明
ほとんどのシステムはデータベースを使用するが…
実行に0.1秒もかかる単体テストは、遅い単体テストである。
『レガシーコード改善ガイド』
瞬時のフィードバックが得られなくなる。
テスト実行時に部品をすり替えてDBアクセスしないようにする。
各ステレオタイプをどうテストするか検討をしておく。
テストするか
クラス | 自動 | 手動 | 説明 |
---|---|---|---|
Action | - | ○ | 画面との結び付きが強い |
Form | ○ | △ | ユニットテストが容易 |
Service | ○ | △ | ロジックの中心。がんばる |
Entity | △ | △ | 自動生成 |
View | - | ○ | レイアウト確認が必要 |
Utility | ○ | △ | ユニットテストが容易 |
いきなりEnd to Endのテストは難易度高い
カバレッジを終了条件にするのは諸刃の剣
テストコードは成果物です。 レビュー しましょう。
頻繁にテストを全実行しなければ、簡単にテストは壊れます。
どう立ち向かっていくか。
以下、具体的なテクニックを『レガシーコード改善ガイド』より。
既存の巨大メソッドに機能追加する必要がある
void 既存メソッド(String input) { // 既存の処理 // : 新規メソッド(input); // 呼び出し // : // 既存の処理 } void 新規メソッド(String input) { // 追加機能 }
public void register(Member member) { // DBに登録 db.insert(member); }
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); }
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("郵便番号が不正です"); } // その他のバリデーション ..... }
@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登録は全く確認していない。
スプラウトメソッドのクラス版
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("郵便番号が不正です"); } // ..... } }
以下のケースでは、スプラウトクラスを使用
現在のシステムの動作を正として、システムの振る舞いを写し取るテスト。
(Characterization Test)
以下、実際にやってみます。
お買上げ額 | 割引率 |
---|---|
~1000円 | 0% |
1000~10000円 | 10% |
10000円~ | 20% |
「価格に負数が渡されて請求額が不正になるバグが発覚した」とする。
⇒レガシーコード改善ガイド 第24章
『もうウンザリです。何も改善できません』
「どうせ90パーセントの時間をヘドロのようなものを相手に過ごすのに、この小さい部分だけきれいにしたところで何だというのだ。もちろん、この小さな部分は改善できる。しかし、それが今日の午後、あるいは明日の私にとって何の役に立つだろうか?」。
『レガシーコード改善ガイド』
しかし、一貫してそのような小さな改善を続ければ、数ヶ月の間にシステムは見違えるような状態になります。ある日の朝、ヘドロを相手に汚い仕事をするつもりで会社に来ると、次のことに気づきます。
『レガシーコード改善ガイド』
「あれ?このコードはいい感じになっているぞ。誰かがこのコードを最近リファクタリングしたようだ」。その時点、すなわち優れたコードと悪いコードの違いを直感的に理解できた時こそが、皆さんが変わる時です。
『レガシーコード改善ガイド』
レガシーシステムを担当する人たちは、しばしば新規開発に携わることを望みます。
<中略>
しかし率直に言って、新規開発には、新規開発なりの問題があります。
『レガシーコード改善ガイド』
私は、数百万行ものレガシーコードを扱っているいくつかのチームを訪れたことがあります。これらのチームは、毎日が挑戦であり、物事をより良くする機会であるととらえ、仕事を楽しんでいました。
『レガシーコード改善ガイド』
新規か保守かは重要でない
導入に向けて…
一緒にがんばりましょう!