システム自動管理ツールとSCMの活用
障害対応や機能追加でシステムの改修を行った後は、開発環境・テスト環境などで動作を確認した後に本番環境に反映するのが一般的です。このとき、アプリケーションの変更は、ビルドとデプロイの方法が定型的なので、環境によって反映が漏れることは少ないです。しかし、ファイル(シェルスクリプトなど)やOS・ソフトウェアの設定(クーロンやWebサーバの設定など)は、環境ごとに手作業で設定することが多く、反映が漏れることがあります。
この問題の解決手段として、システム自動管理ツールとSCMを組み合わせて運用する方法があります。
システム自動管理ツールは、OS上の環境に対する定義情報を元に、実際の環境設定を自動的に行ってくれるツールです。代表的なツールであるPuppetの場合、以下のような環境設定が自動化できます。
- ソフトウェア(基本的にaptやyumで管理されてるもの)のインストールやプロセスの起動
- OSのユーザの設定
- クーロンの設定
- 任意のファイルのコピー・変更
など
SCM(software configuration management)は、いわゆるバージョン管理ツールのことで、SubversionやGitが代表的です。
システム自動管理ツールとSCMの活用イメージ
システム自動管理ツールとSCMを活用した場合のイメージは以下になります。
環境の定義情報をSCMで管理し、各環境は、SCM上の定義情報を利用します。これにより、同じ定義情報をもとに各環境をツールが自動で設定するため、設定のズレや漏れを無くすことができます。また、環境ごとに定義情報のバージョンを変えることができるので、「テスト環境でテストが終わるまでは本番環境に設定を反映しない」という運用ができます。また、任意のバージョンの環境に戻すことも容易ですし、新たに環境を構築するのも容易です。
導入時の課題
概念的な説明は簡単ですが、実際の導入は簡単ではありません。ツールの知識も当然必要ですが、「どこまでツールに管理させるか?」のさじ加減も大事です。ツールの機能を駆使すれば、さまざまな設定を管理できますし、環境ごとの差異(本番環境の場合だけ設定を変えるなど)も吸収することができます。しかし、その分、環境の定義情報が複雑になり、メンテナンス性が悪くなります。
また、システム自動管理ツールとSCMを連携させる手段も、適切な方法を選択する必要があります。例えば、Puppetの場合、環境別に定義情報を管理するEnvironmentsという機能が備わっていますが、個人的には、Environments機能は使ない方がシンプルだと思います。具体的には、こちらのサイトで紹介されている方法が参考になります(恐らくEnvironments機能は使っていないと思います)。
さいごに
導入は簡単ではないとはいえ、設定を自動化するメリットは大きいので、「ここの部分の設定が自動化できると有難い」という部分があれば、積極的に導入を検討すべきだと思います。
既存システムに対してテストコードを書く(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メソッドは、リファクタリング前と比べて可読性が上がっています。
既存システムに対してテストコードを書く(1/2)
JUnitを始めとするテストツールが充実した現在も、運用・保守の現場では、テストコードが存在しないJavaの既存システムを目にすることがあります。このようなシステムに対して、後付でテストコードを書こうとすると、大抵は以下の問題に突き当たって、モックの作成が難しくなります。
- テスト対象のコードが、staticメソッドを呼び出している
- テスト対象のコードが、技術的なAPIをnewして使っている
例えば、次のようなコードです。
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メソッドがテスト対象のコードです。
メソッドの初めに、DBAdapterクラスのstaticメソッドのgetCalcDaysFromDBメソッドを呼び出して、日数を取得しています。getCalcDaysFromDBメソッドは、JDBCを使ってデータベースにアクセスします。テストするにはデータベースを準備する手間が発生しますので、getCalcDaysFromDBメソッドをモックに変更したいところです。
次に、BufferedReaderクラスやFileReaderクラスをnewしてオブジェクトを作成し、実質年率の値をファイルから読み取っています。テストするにはファイルの準備が必要になるため、これらのオブジェクトもモックに変更したいところです。
JMockItを使った解決
著名なモックフレームワークのEasyMockやMockitoでは、この問題は解決できませんが、後発の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); } }
NonStrictExpectationsというクラスは、JMockItが提供するクラスです。詳しい説明は省略しますが、DBAdapter.getCalcDaysFromDBメソッドと、BufferedReaderとFileReaderがモックにすり替えられています。
JMockItの解説は、ryoasaiさんやj5ik2oさんのブログに詳しく書かれていますので、興味のある方は参照することをお勧めします。
では、JMockItをどんどん使って既存システムのテストコードを書けばいいかと言うと、そうでもありません。「力の悪用の例(英語の原文はAn example of power corrupting)」というサブタイトルのModern Mocking Tools and Black Magicという記事に興味深い問題の指摘が書いてあります。
その内容については、次回のブログで触れたいと思います。
要求レベルのテストの自動化
ユーザーの要求が満たされてることをテストときは、要求を元に開発者やテスターがテスト項目を作成し、画面を操作しながら挙動を目視で確認するのが一般的だと思います。しかし、この方法だと、要求とテスト項目を紐づける手間がかかったり、テストが手動なので回帰テストが大変になります。
そのような状況を解決する技術として、「ユーザーと開発者が要求の記述を共有し、その記述を入力にしてテストプログラムを実行する」ツールが登場しています。これらのツールを使えば、要求レベルのテストが自動化できる訳です。
では、ユーザーと開発者が共有する要求の記述とはどのようなものか見てみましょう。
Feature: 車の燃料の補給 運転手は、車を走らせるために、燃料を補給する必要がある。 Scenario: 燃料の補給 Given 車のタンクには、10リットルの燃料が入っている When 運転手は、50リットルの燃料を補給した Then タンクには、60リットルの燃料が入っている
この記述は、BDDのツールとして有名なCucumberでの要求の記述です。
英単語の「Feature」には機能名と機能の説明、「Scenario」にはシナリオを書きます。「Scenario」の中で、1行づつステップを記述します。ステップの頭文字の「Given」は前提条件、「When」は処理の実行、「Then」は事後条件の意味合いになります。この記述であれば、ユーザーと開発者が共有できる内容だと思います。
次に、テストコードを見てましょう。
public class FuelCarSteps { private Car car; @Given("^車のタンクには、(\\d*)リットルの燃料が入っている$") public void createCar(int initialFuelLevel) { car = new Car(initialFuelLevel); } @When("^運転手は、(\\d*)リットルの燃料を補給した$") public void addFuel(int addedFuel) { car.addFuel(addedFuel); } @Then("^タンクには、(\\d*)リットルの燃料が入っている$") public void checkBalance(int expectedFuelLevel) { int actualFuelLevel = car.getFuelLevel(); assertThat(actualFuelLevel, is(expectedFuelLevel)); } }
このコードは、CucumberをJavaで実装したCucumber-JVMでのテストコードです(Cucumber自体はRubyで実装されています)。
シナリオのステップごとにメソッドを用意し、中身を実装します。詳しい説明は省略しますが、なんとなく、やってることは伝わるかと思います。また、Seleniumと連携して画面レベルでのテストも可能です。
なお、Cucumber-JVMは、JUnitを使用してるので、テストの実行結果はJUnitの画面で表示されます。
- 導入時の課題
要求レベルのテストの自動化できれば、手動のテストが無くなるかというと、そうはならないと思います。特に、大事な業務で使用するようなシステムでは、自動化したテストが成功したからそのまま本番リリースというのは想像できません。また、画面の見栄えや使い易さ、帳票の印字チェックなどは、最終的に手動でテストせざるを得ません。
とはいえ、要求レベルのテストの自動化ができれば、回帰テストが充実するし、要求漏れのバグも少なくなるでしょう。現場の状況に合わせて、手動と自動の範囲や棲み分けを明確にすることが大事だと思います。