JUnit 実践入門 体系的に学ぶユニットテストの技法 - 第8章 パラメータ化テスト テストケースとテストデータの分離

第8章 パラメータ化テスト テストケースとテストデータの分離

  • 異なる入力値について似たようなテストを実施する場合など、テストケース毎の差異が入力値と期待値だけ、のような状況下において、入力値と期待値をパラメータとして外部化することで見通しを良くする

8.1 テストデータの選択

  • 同値クラス
  • 組み合わせによるテストデータ選択
    • 「プログラムに混入する不具合のほとんどは、単項目に関連する不具合」
    • よって「条件をすべて満たす、あるいは1つだけ満たさない」でテストデータを選択すると幸せ
      • すべて満たす → true返すはずが、条件1つバグってるの検知できる
      • 1つだけ満たさない → false返すはずが、条件1つバグってるの検知できる
  • 境界値も組み合わせるとよりイイね

8.2 入力値と期待値のパラメータ化

  • テストデータの組み合わせによりテストコードが冗長化するのを防ぐ、パラメータ化テスト parameterized test
    • テストデータとテストメソッドを分割する

Theories - パラメータ化テストのテストランナー

  • @RunWith でテストクラスに付与する

@Theory - テストメソッドに指定するアノテーション

  • @Test の代わり
  • 任意の引数を宣言できる

@DataPoint - パラメータを指定するアノテーション

  • パラメータは static かつ public なフイールドで定義する
package ch08;

import org.junit.experimental.theories.DataPoint;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;

@RunWith(Theories.class)
public class ParameterizedTest {
    @DataPoint
    public static int INT_PARAM_1 = 3;
    @DataPoint
    public static int INT_PARAM_2 = 4;
    
    public ParameterizedTest() {
        System.out.println("初期化");
    }
    
    @Theory
    public void 引数を持つテストメソッド(int param) throws Exception {
        System.out.println("引数を持つテストメソッド (" + param + ")");
        
    }
}
初期化
引数を持つテストメソッド (3)
初期化
引数を持つテストメソッド (4)
  • @DataPoint 指定した値が渡さている
  • 「初期化」が二回出てきてる → コンストラクタが二回呼ばれてる
    • 都度都度オブジェクトが生成されている
package ch08;

import org.junit.experimental.runners.Enclosed;
import org.junit.experimental.theories.DataPoint;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;

@RunWith(Enclosed.class)
public class EnclosedParameterizedTypeTest {
    @RunWith(Theories.class)
    public static class intのパラメータ化テスト {
        @DataPoint
        public static int INT_PARAM_1 = 3;
        @DataPoint
        public static int INT_PARAM_2 = 4;
        
        @Theory
        public void 引数がint型のテストメソッド(int param) throws Exception{
            System.out.println("引数がint型のテストメソッド (" + param + ")");
        }
    }

    @RunWith(Theories.class)
    public static class Stringのパラメータ化テスト{
         @DataPoint
         public static String STRING_PARAM_1 = "Hello";
         @DataPoint
         public static String STRING_PARAM_2 = "World!";

         @Theory
         public void 引数がString型のテストメソッド(String param) throws Exception {
             System.out.println("引数がString型のテストメソッド (" + "param" + ")");
         }
    }
}
引数がString型のテストメソッド (param)
引数がString型のテストメソッド (param)
引数がint型のテストメソッド (3)
引数がint型のテストメソッド (4)

 
 

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit 実践入門 体系的に学ぶユニットテストの技法 - 第7章 テストフィクスチャ テストデータや前提条件のセットアップ

第7章 テストフィクスチャ テストデータや前提条件のセットアップ

7.1 テストフィクチャとは

  • テストで扱う、データやテスト実行環境、オブジェクトの状態など

ユニットテストのテストフィスクチャ

  • テスト対象オブジェクト
  • 入力値
  • 期待値
  • テスト実行までに必要な、テストオブジェクトの操作
  • ファイルなど外部リソース
  • データベースやソケットサーバなど、外部システム
  • 依存クラスや依存外部システムのモックオブジェクト

  • 一般にテスト実行フェーズよりも、セットアップのコードのほうが長くなる

フレッシュフィクスチャ

  • フィクスチャはテストケースごとに独立し、テスト実行ごとに初期化され、終了時に開放する → フレッシュフィクスチャ戦略

フィクスチャとスローテスト問題

  • フィクスチャのセットアップに長い時間がかかること → スローテスト問題 slow tests
    • DB のテーブルデータ全レコード削除とかやってるから
  • テストの並列実行、共有フィクスチャ、カテゴリ化テストなどの解決策

共有フィクスチャ

  • 共有フィクスチャは、スローテスト問題 shared fixture の解法の一つ
  • テストケースごとの独立性が弱くなる問題
  • テストコードのメンテナンス性低下。あるフィクスチャを参照しているテストケースは、全部でどれだけ??

7.2 フィクスチャのセットアップパターン

インラインセットアップ

  • テストメソッドごとにフィクスチャのセットアップを行う
  • コードでの見通しが良くなるが、長すぎると可読性は下がる
    • 20 行がひとつの目安

暗黙的セットアップ

  • Before アノテーションが付与されたメソッド(セットアップメソッド)のこと
    • テストメソッド実行前に暗黙的 implicit に実行されるからね
    • 各初期化処理の共通部分
  • Enclosed テストランナーと相性良し
    • 構造化!共通前処理!インナークラス!

生成メソッドでのセットアップ

  • テストクラスをまたいで暗黙的セットアップは出来ない…
    • 共通した初期化処理を独立したクラスのメソッドに抽出する 生成メソッドパターン
  • staticインポートできるように、独立クラスにstaticで書くと幸せ。日本語メソッド名で内容を表示してあげるともっといいね

外部リソースからのセットアップ

  • Java はデータを生成するコードま記述か苦手。もっさりする。

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit 実践入門 体系的に学ぶユニットテストの技法 - 第6章 テストのコンテキスト テストケースの構造化

第6章 テストのコンテキスト テストケースの構造化

6.2 テストケースの整理

  • メソッド単位で整理して個別クラス作成するよりも、初期化処理で整理するほうが、一般に良い
    • 前者は初期化処理を共通化できないため

6.3 コンテキストのパターン

  • 共通のデータに着目する
    • たいてい、同一のデータに対して、削除なり挿入なりしている
  • 共通の状態に着目する
    • ArrayList のテスト時を想定。テスト前にある状態をセットアップしているはず
  • コンストラクタのテストを分ける
    • テスト対象オブジェクトの生成のテスト。初期状態を検証する、特別なコンテキスト。

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit 実践入門 体系的に学ぶユニットテストの技法 - 第5章 テストランナー テスト実行方法の制御

第5章 テストランナー テスト実行方法の制御

5.1 コマンドラインからの JUnit の実行

  • コマンドラインからも JUnit テストを実行できる
  • JUnitCore クラスの main メソッドでは以下を行っている
    1. テストケースの収集
    2. テストの実行
    3. テスト結果の出力(レポート)

5.2 テストランナーとは

  • テストケースをどのように実行するか、制御するもの
  • 全実行、絞り込み、除外などが行える
  • テストクラスごとに設定。org.junit.runner.RunWith アノテーション

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

明解 Javaによるアルゴリズムとデータ構造 6-9_度数ソート

6-9 度数ソート

  • 分布数え上げソートとも呼ばれる
  • 要素の大小関係を比較しない
  • 安定

度数ソート

学生9人の10点満点テストを例に

  1. 度数分布表の作成 - 各点数の学生が何人いるか調べる
  2. 累積度数分布表の作成 - 0点からその点数までに何人の学生がいるか調べる
  3. 目的配列の作成
  4. 配列のコピー

明解 Javaによるアルゴリズムとデータ構造

明解 Javaによるアルゴリズムとデータ構造

JUnit 実践入門 体系的に学ぶユニットテストの技法 - 第4章 アサーション 値を比較検証するしくみ

第4章 アサーション 値を比較検証するしくみ

4.1 Assert による値の比較検証

  • org.junit.Asset にアサーションメソッドが多数定義されている
    • static インポートされることを想定した static メソッド
  • assertThat と fail 2つ知っていれば十分。

assertThat - 汎用的な値の比較検証

  • JUnit では、比較のルールと比較検証のしくみを分離している
    • ルール: Matcher オブジェクト。場合によってカスタマイズも出来る
    • しくみ: assertThat メソッド。仕組み自体はそのまま使いまわしていける。

fail - テストを失敗させる

4.2 Matcher API によるアサーションの特徴

  • Matcher API は Hamcrest という、JUnit の独立した拡張ライブラリの一つだったが、統合された
  • 以前は assertEquals メソッドに比較ルールを実装していたが、柔軟性や可読性に欠けた
  • 可読性の高い記述
  • 柔軟な比較
  • 詳細な情報の提供

4.3 Matcher API の利用

  • org.hamcrestCoreMatchers クラスや、org.junit.matchers.JUnitMatchers クラスなどに定義された、static なファクトリメソッドから Matchers オブジェクトを生成する。

CoreMatchers が提供する Matcher

  • is
    • 独自実装クラスのオブジェクトに対して利用する場合は、適切な equals メソッドがオーバーライドされているか要確認。基本はデフォルト equals が使われるため、予期しない結果になるかも。
  • nullValue
  • not
  • notNullValue
  • sameInstance
    • == 演算子による同一性比較
    • ボクシング変換(プリミティブ型 -> オブジェクト型)に注意
      assertThat(128, is(sameInstance(128)))
      は失敗する
      • JVM の仕様上128 の Integer オブジェクトが、期待値と実測値のそれぞれで生成されるため
  • instanceOf
    • 実測値が null の場合は必ず失敗

JUnitMatchers が提供する Matcher

  • hasItem
  • hasItems

カスタム Matcher の作成

  • 日付をざっくり比較する(ミリ秒とかいらない) Matcher をつくる

手順

isDate クラスの作成

  • BaseMtcher クラスを extends

    ファクトリメソッド作成

  • static インポートにより、自然言語に近い形で表記したいため
  • プロジェクトで複数のカスタム Matcher 作るなら、ファクトリメソッドをまとめて定義したクラスを作成すると吉

    コンストラクタの定義

    matches メソッドの実装

  • 実測値を引数に取り、一致なら true、そうでなければ false を返す
  • 一致しない場合には、describeTo メソッドが呼び出される

    discribeTo メソッドの実装

  • フレームワークに比較検証が失敗した理由を通知する

    テスト失敗メッセージの確認

  • ここを見て原因がすぐに分かるよう、describeTo を工夫する

ソースコード

プロダクションコード

package ch04;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;

public class IsDate extends BaseMatcher {

    private final int yyyy;
    private int mm;
    private int dd;
    Object actual;

    IsDate(int yyyy, int mm, int dd) {
        this.yyyy = yyyy;
        this.mm = mm;
        this.dd = dd;
    }

    @Override
    public boolean matches(Object actual) {
        this.actual = actual;
        if (!(actual instanceof Date))
            return false;
        Calendar cal = Calendar.getInstance();
        cal.setTime((Date) actual);
        if (yyyy != cal.get(Calendar.YEAR))
            return false;
        if (mm != cal.get(Calendar.MONTH) + 1)
            return false;
        if (yyyy != cal.get(Calendar.DATE))
            return false;
        return true;
    }

    @Override
    public void describeTo(Description desc) {
        desc.appendValue(String.format("%d/%02d/%02d", yyyy, mm, dd));
        if (actual != null) {
            desc.appendText(" but actual is )");
            desc.appendValue(new SimpleDateFormat("yyyy/MM/dd").format((Date) actual));
        }
    }
    
    public static Matcher dateOf(int yyyy, int mm, int dd) {
        return new IsDate(yyyy, mm, dd);
    }
}

テストコード

package ch04;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static ch04.IsDate.*;

import java.util.Date;

import org.junit.Test;

public class IsDateTest {

    @Test
    public void 日付の比較() {
        assertThat(new Date(), is(dateOf(2012, 1, 12)));
}

}

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

明解 Javaによるアルゴリズムとデータ構造 6-8_ヒープソート

6-8 ヒープソート

ヒープ

  • 親の値が子の値以上である、完全2分木のこと
  • 兄弟での大小関係は任意
    • ヒープのことを別名、半順序木 partial ordered tree ともいう
  • a[i] に対して、
    • 親は a[(i - 1) / 2]
    • 左の子は a[i * 2 + 1] ※剰余切り捨て
    • 右の子は a[i * 2 + 2]

根を削除したヒープの再構築

  1. 根を取り出す
  2. 最後の要素(最下流の最も右側に位置する要素)を根に移動する
  3. 自分より大きい方の子と交換して一つ下流に降りる作業を、根から始めて、以下の条件何れかが成立するまで繰り返す
    • 子の方が値が小さい
    • 葉に到達した

ヒープソートへの拡張

  1. 変数 i の値を n - 1 で初期化する
  2. a[0] と a[i] を交換する
  3. a[0], a[1], …, a[i - 1] をヒープ化する
  4. i の値をデクリメントして 0 になれば終了
    そうでなければ2に戻る

配列のヒープ化

ソースコード

import java.util.Scanner;

class HeapSort {
    static void swap(int[] a, int idx1, int idx2) {
        int t = a[idx1];
        a[idx1] = a[idx2];
        a[idx2] = t;
    }

    static void downHeap(int[] a, int left, int right) {
        int temp = a[left];
        int child;
        int parent;

        for (parent = left; parent < (right + 1) / 2; parent = child) {
            int cl = parent * 2 + 1;
            int cr = cl + 1;
            child = (cr <= right && a[cr] > a[cl]) ? cr : cl;
            if (temp >= a[child]) {
                break;
            }
            a[parent] = a[child];
        }
        a[parent] = temp;
    }

    static void heapSort(int[] a, int n) {
        for (int i = (n - 1) / 2; i >= 0; i--) {
            downHeap(a, i, n - 1);
        }

        for (int i = n - 1; i > 0; i--) {
            swap(a, 0, i);
            downHeap(a, 0, i - 1);
        }
    }

    public static void main(String[] args) {
        Scanner stdIn = new Scanner(System.in);
        System.out.println("ヒープソート");
        System.out.print("要素数: ");
        int nx = stdIn.nextInt();
        int[] x = new int[nx];

        for (int i = 0; i < nx; i++) {
            System.out.print("x[" + i + "]: ");
            x[i] = stdIn.nextInt();
        }

        heapSort(x, nx);

        System.out.println("昇順にソートしました。");
        for (int i = 0; i < nx; i++) {
            System.out.println("x[" + i + "]: " + x[i]);
        }
    }
}

ちょい飽きたので飛ばし

明解 Javaによるアルゴリズムとデータ構造

明解 Javaによるアルゴリズムとデータ構造