既存システムに対してテストコードを書く(2/2)

前回のブログでは、既存システムに後付でテストコードを書くときにJMockItが有用なことを紹介しました。

今回のブログでは、Brettという人が書いたModern Mocking Tools and Black Magic<サブタイトル:An example of power corrupting(力の悪用の例)>の記事(以下、「Brettさんの記事」と呼びます)を参考にしながら、JMockIt(および、他のモックフレームワークも同様)を活用する際の留意点を紹介したいと思います。

Brettさんの記事の言わんとすることは、私なりに要約すると、「JMockItのような高機能なモックフレームワークは、リファクタリングという根本解決を遠ざけてしまう」という問題提起です。

Brettさんの記事では、問題提起と合わせて、テストし易いコードへのリファクタリングのテクニックが紹介されています。そこに出てくるテクニックを使って、前回のブログで紹介した以下のソースコードリファクタリングしてみます(前回のブログを事前に読んでいることを前提に説明します)。

public class InterestCalclator {
	public int calcInterest(int principal) {

		// 日数を取得
		int days = DBAdapter.getCalcDaysFromDB();

		// 実質年率の値をファイルから取得
		BigDecimal interestRate = null;
		try {
			BufferedReader br = new BufferedReader(new FileReader("interestRate.txt"));
			String line = br.readLine();
			interestRate = new BigDecimal(line);
			br.close();
		} catch (IOException ex) {
			ex.printStackTrace();
		}

		// 利子を計算
		BigDecimal principalDec = BigDecimal.valueOf(principal);
		BigDecimal daysDec = BigDecimal.valueOf(days);
		BigDecimal result = principalDec.multiply(interestRate).multiply(daysDec.divide(BigDecimal.valueOf(365), 5, BigDecimal.ROUND_HALF_EVEN));
		return result.round(new MathContext(0, RoundingMode.HALF_EVEN)).intValue();
	}
}

public class DBAdapter {
	public static int getCalcDaysFromDB() {
		int days = 0;
		// JDBCを使って、データベースから値を取得
		// 処理の記述は省略します
		return days;
	}
}

モックにしたい処理を部分的にメソッドとして抽出する

テスト対象であるInterestCalclatorクラスのcalcInterestメソッドの中で、日数を取得する部分(DB絡みのstaticメソッドの呼び出し)と、実質年率の値をファイルから取得する部分をメソッドとして抽出します(それぞれ、getClacDaysメソッドとgetInterestRateメソッド)。

public class InterestCalclator {
	public int calcInterest(int principal) {
		// 日数を取得
		int days = getCalcDays();

		// 実質年率の値をファイルから取得
		BigDecimal interestRate = getInterestRate();

		// 利子を計算
		BigDecimal principalDec = BigDecimal.valueOf(principal);
		BigDecimal daysDec = BigDecimal.valueOf(days);
		BigDecimal result = principalDec.multiply(interestRate).multiply(daysDec.divide(BigDecimal.valueOf(365), 5, BigDecimal.ROUND_HALF_EVEN));
		return result.round(new MathContext(0, RoundingMode.HALF_EVEN)).intValue();
	}
	
	protected int getCalcDays() {
		return DBAdapter.getCalcDaysFromDB();
	}
	
	protected BigDecimal getInterestRate() {
		BigDecimal interestRate = null;
		try {
			BufferedReader br = new BufferedReader(new FileReader("interestRate.txt"));
			String line = br.readLine();
			interestRate = new BigDecimal(line);
			br.close();
		} catch (IOException ex) {
			ex.printStackTrace();
		}
		return interestRate;
	}
}

これにより、テスト時に、抽出したメソッドをオーバーライドしてモックのメソッドにすることができます。

リファクタリング後のテストコード

リファクタリングをしたコードに対するテストコードは以下になります。

public class InterestCalclatorTest {
	class InterestCalclatorTestingSubclass extends InterestCalclator {
		@Override
		protected int getCalcDays() {
			return 30;
		}

		@Override
		protected BigDecimal getInterestRate() {
			return new BigDecimal("0.032");
		}
	}
	
	@Test
	public void testCalcInterest() throws Exception {
		InterestCalclator target = new InterestCalclatorTestingSubclass();
		int interest = target.calcInterest(1000000);
		assertEquals(2630, interest);
	}
}

JMockItを使用したテストコードとの比較

前回のブログで紹介したJMockItを使用したテストコードを見てみましょう。

public class InterestCalclatorTest {
	@Test
	public void testCalcInterest() throws Exception {
		new NonStrictExpectations(DBAdapter.class) {
			{
				DBAdapter.getCalcDaysFromDB();
				result = 30;
			}
		};
		new NonStrictExpectations() {
			BufferedReader br;
			FileReader fr;
			{
				br.readLine();
				result = "0.032";
			}
		};
		InterestCalclator target = new InterestCalclator();
		int interest = target.calcInterest(1000000);
		assertEquals(2630, interest);
	}
}

JMockItを使用したテストコードの方は、リファクタリング後のテストコードに比べて、テスト対象の処理の細部の知識を必要とします(BufferedReaderの何のメソッドを呼んでいるかなど)。テストコードを記述する開発者への負担になりますし、テストコードが、テスト対象の処理に不要に依存してしまいメンテナンス性が悪くなります(BufferedReaderを使わなくなったら、テストコードを修正する必要があるなど)。

また、リファクタリングしたInterestCalclatorのcalcInterestメソッドは、リファクタリング前と比べて可読性が上がっています。

まとめ

既存システムに対してテストコードを書くときは、まずは、テストし易いコードにリファクタリングすることを検討すべきだと思います。しかし、実際には、事情があってリファクタリングができない場合(サードパーティ製のプログラムや、障害が起きたらニュースに載るようなシステムなど)もあると思いますので、そのときは、JMockItを使用するとよいでしょう。