読書録『知識ゼロから学ぶソフトウェアテスト 【改訂版】』

知識ゼロから学ぶソフトウェアテスト 【改訂版】

知識ゼロから学ぶソフトウェアテスト 【改訂版】

読んで大事だと思った箇所を、業務SE視点で解釈する。

  • テスト計画

テストでも何でもそうだが、先ず始めに計画ありき。
テスト計画書はIEEE829のテストテンプレートをカスタマイズする。

計画に盛り込むリスクは下記の式で算出。
 リスク=問題の発生確率×発生した場合のダメージ
典型的なリスク要因として、複雑度/新規性/大きな変更/上位依存性(未出荷の製品やコンポーネントに対する依存関係)がある。

  • テスト実施

やるべきテストは下記3つ。
1. 境界値テスト
2. デシジョンテーブルでのパターン網羅
3. 状態遷移(ただし、アーキテクチャ設計、デザインパターンの適用でバグを抑えられる)

  • レビュー

テスト仕様書や結果のレビューが追いつかない場合、バグがありそうな箇所を重点的に攻める。
1. 複雑度メトリクスの高いもの
  McCabeサイクロマティック数の複雑度=プログラムに含まれるルートの数−分岐点の数+2
   →この数よりもテストケース数が少ないのは論外
    21〜50は高リスク、50〜はテスト不能らしいが、50を超えるプログラムはざらにあるが・・・
2. 頻繁に変更されているコード(Googleのバグ予測システムでも使われている)

  • 管理

「バグ曲線が寝た」→「出荷して問題ない」という論理は弱い。
テストの終盤になってバグが増加するケースもあるので、その場合は下記の指標を取り入れる。
1. 重要度の高いバグが減ってきているか(→出荷可能なレベルになっているか)
2. バグ修正にかかる時間が短くなってきているか(→プログラマが忙しすぎて修正に手が回らない、といった事態ではないか)
3. コンポーネント毎のバグ数をみて分析(→20%のコードに80%のバグがある)

  • その他

・テストの自動化
概して上手くいかない。特に、画面系の自動化は止めた方がいい。メンテのコストが高すぎる。
逆に、共通機能とかAPI的なものは自動化した方がいい。

・探索的テスト
ソフトウェアの理解+テスト設計+テスト実施を同時に行う手法。バグ発見効率が良い。
一個人の責任として、きちんと仕様を理解して、テスト設計し、テストし、バグ報告するスタイル。
 →当たり前のことすぎるが、実際のところ、これができない人が多すぎて困る。
テスト仕様書は必ずしも必要ではない。下記のようなテストも取り入れる。
 - 弱いところを見つけて重点的にテストする
 - 実行環境を変えてテストしてみる(ブラウザを変えるとか)
 - 重要機能に対してシンプルなテストを実施し、デグレしてないことを確認する

→うまいこと計画に明記し、うまいことテスト内容とエビデンスを残さないと、実際のプロジェクトに取り入れるのは難しいと思う。
 重役を説得するにはどうすればよいだろうか・・・

SELECT結果の操作(変数宣言、カーソル、配列)

  • 最もシンプルなPL/SQLのプログラム
BEGIN
  NULL;
END;
  • 変数と定数
DECLARE
  a VARCHAR2(10);
  b VARCHAR2(10)          := 'bbb';
  c VARCHAR2(10) NOT NULL := 'ccc';
  d CONSTANT VARCHAR2(10) := 'ddd';
BEGIN
  dbms_output.put_line('a : ' || a);
  dbms_output.put_line('b : ' || b);
  dbms_output.put_line('c : ' || c);
  dbms_output.put_line('d : ' || d);
END;

実行結果

a : 
b : bbb
c : ccc
d : ddd
  • SELECT結果の代入(単一項目のSELECT INTO)
DECLARE
  now DATE;
BEGIN
  SELECT sysdate INTO now FROM dual;
  dbms_output.put_line('now : ' || now);
END;

実行結果

now : 14-02-04
  • SELECT結果の代入(複数項目のSELECT INTO)
DECLARE
  yyyymmdd CHAR(8);
  hh24miss CHAR(6);
BEGIN
  SELECT TO_CHAR(sysdate, 'YYYYMMDD'),
    TO_CHAR(sysdate, 'HH24MISS')
  INTO yyyymmdd,
    hh24miss
  FROM dual;
  dbms_output.put_line('yyyymmdd : ' || yyyymmdd);
  dbms_output.put_line('hh24miss : ' || hh24miss);
END;

実行結果

yyyymmdd : 20140204
hh24miss : 234759
  • カーソルとループ(LOOP:後判定反復処理)
DECLARE
  CURSOR c_emp
  IS
    SELECT * FROM emp WHERE rownum < 3;
  r_emp c_emp%rowtype;
BEGIN
  OPEN c_emp;
  LOOP
    FETCH c_emp INTO r_emp;
    EXIT
  WHEN c_emp%notfound;
    dbms_output.put_line(r_emp.ename);
  END LOOP;
  CLOSE c_emp;
END;

実行結果

SMITH
ALLEN
  • カーソルとループ(WHILE:前判定反復処理)
DECLARE
  CURSOR c_emp
  IS
    SELECT * FROM emp WHERE rownum < 3;
  r_emp c_emp%rowtype;
BEGIN
  OPEN c_emp;
  FETCH c_emp INTO r_emp;
  WHILE c_emp%found
  LOOP
    dbms_output.put_line(r_emp.ename);
    FETCH c_emp INTO r_emp;
  END LOOP;
  CLOSE c_emp;
END;

実行結果

SMITH
ALLEN
  • カーソルとループ(FORループ)
DECLARE
  CURSOR c_emp
  IS
    SELECT * FROM emp WHERE rownum < 3;
  r_emp c_emp%rowtype;
BEGIN
  OPEN c_emp;
  FETCH c_emp INTO r_emp;
  FOR i IN 1..2
  LOOP
    dbms_output.put_line(r_emp.ename);
    FETCH c_emp INTO r_emp;
  END LOOP;
  CLOSE c_emp;
END;

実行結果

SMITH
ALLEN
  • カーソルとループ(カーソルFORループ)
DECLARE
  CURSOR c_emp
  IS
    SELECT * FROM emp WHERE rownum < 3;
  r_emp c_emp%rowtype;
BEGIN
  FOR r_emp IN c_emp
  LOOP
    dbms_output.put_line(r_emp.ename);
  END LOOP;
END;

実行結果

SMITH
ALLEN
DECLARE
  CURSOR c_emp
  IS
    SELECT * FROM emp WHERE rownum < 3;
type t_emp
IS
  TABLE OF emp.job%type INDEX BY emp.ename%type;
  w_emp t_emp;
  i emp.ename%type;
BEGIN
  --キーにename、値にjobを格納する
  FOR r_emp IN c_emp
  LOOP
    w_emp(r_emp.ename) := r_emp.job;
  END LOOP;
  --連想配列のループ
  i       := w_emp.first;
  WHILE i IS NOT NULL
  LOOP
    dbms_output.put_line(i || ' : ' || w_emp(i));
    i := w_emp.next(i);
  END LOOP;
END;

実行結果

ALLEN : SALESMAN
SMITH : CLERK
  • ネストした表
DECLARE
  CURSOR c_emp
  IS
    SELECT * FROM emp WHERE rownum < 3;
type t_emp
IS
  TABLE OF emp.ename%type;
  w_emp t_emp := t_emp(); --ネストした表を宣言し、同時に初期化する
BEGIN
  --enameをネストした表に格納する
  FOR r_emp IN c_emp
  LOOP
    w_emp.extend;
    w_emp(w_emp.last) := r_emp.ename;
  END LOOP;
  --ネストした表のループ
  FOR i IN w_emp.first .. w_emp.last
  LOOP
    dbms_output.put_line(i || ' : ' || w_emp(i));
  END LOOP;
END;

実行結果

1 : SMITH
2 : ALLEN
  • VARRAY
DECLARE
  CURSOR c_emp
  IS
    SELECT * FROM emp WHERE rownum < 3;
type t_emp IS varray(10) OF emp.ename%type; --最大サイズを指定(VARRAYは拡張不可)
w_emp t_emp := t_emp();                     --VARRAYを宣言し、同時に初期化する
BEGIN
  --enameをVARRAYに格納する
  FOR r_emp IN c_emp
  LOOP
    w_emp.extend;
    w_emp(w_emp.last) := r_emp.ename;
  END LOOP;
  --VARRAYのループ
  FOR i IN w_emp.first .. w_emp.last
  LOOP
    dbms_output.put_line(i || ' : ' || w_emp(i));
  END LOOP;
END;

実行結果

1 : SMITH
2 : ALLEN
  • BULK COLLECT
DECLARE
  CURSOR c_emp
  IS
    SELECT * FROM emp WHERE rownum < 3;
type t_emp_tab
IS
  TABLE OF c_emp%rowtype INDEX BY pls_integer;
  w_emp_tab t_emp_tab;
BEGIN
  OPEN c_emp;
  FETCH c_emp bulk collect INTO w_emp_tab;
  CLOSE c_emp;
  --連想配列のループ
  IF w_emp_tab.count = 0 THEN
    dbms_output.put_line('data not found');
  ELSE
    FOR i IN w_emp_tab.first .. w_emp_tab.last
    LOOP
      dbms_output.put_line(w_emp_tab(i).empno || ' : ' || w_emp_tab(i).ename || ' : ' || w_emp_tab(i).job);
    END LOOP;
  END IF;
END;

実行結果

7369 : SMITH : CLERK
7499 : ALLEN : SALESMAN

※実行環境は下記の通りです。

SQL> select * from v$version;

BANNER
                                                                                                                                                              • -
Oracle Database 12c Enterprise Edition Release 12.1.0.1.0 - 64bit Production PL/SQL Release 12.1.0.1.0 - Production CORE 12.1.0.1.0 Production TNS for 64-bit Windows: Version 12.1.0.1.0 - Production NLSRTL Version 12.1.0.1.0 - Production

※コードはSQL Developerで整形しています。
 ショートカットキー:Ctrl+Shift+F7

血の報い

(新共同訳)
42:21 互に言った。
「ああ、我々は弟のことで罰を受けているのだ。弟が我々に助けを求めたとき、あれほどの苦しみを見ながら、耳を貸そうともしなかった。それで、この苦しみが我々にふりかかった。」
42:22 するとルベンが答えた。
「あのときわたしは、『あの子に悪いことをするな』と言ったではないか。お前たちは耳を貸そうともしなかった。だから、あの子の血の報いを受けるのだ。」

(TEV)
42:21 and said to one another, "Yes, now we are suffering the consequences of what we did to our brother; we saw the great trouble he was in when he begged for help, but we would not listen. That is why we are in this trouble now."
42:22 Reuben said, "I told you not to harm the boy, but you wouldn't listen. And now we are being paid back for his death."

(KJV)
42:21 And they said one to another, We are verily guilty concerning our brother, in that we saw the anguish of his soul, when he besought us, and we would not hear; therefore is this distress come upon us.
42:22 And Reuben answered them, saying, Spake I not unto you, saying, Do not sin against the child; and ye would not hear? therefore, behold, also his blood is required.

ねたみ

(新共同訳)
26:14 多くの羊や牛の群れ、それに多くの召し使いを持つようになると、ペリシテ人はイサクをねたむようになった。

(TEV)
26:14 Because he had many herds of sheep and cattle and many servants, the Philistines were jealous of him.

(KJV)
26:14 For he had possession of flocks, and possessions of herds, and great store of servants: and the Philistines envied him.

スレッドセーフではないコード(4回目)

今回はsynchronizedメソッドの話。「synchronizedが付いているメソッドは同時に実行されない」と思っていたら大まちがい。

下記に4種類のコード例のうち、System.out.println()が排他的に実行されるのは2つだけ。
System.outする箇所にブレークポイントを設定してデバッグすると動作がよく分かる。

SynchronizedTest.java

public class SynchronizedTest {
	public static void main(String[] args) {
		SomeThread someThread = new SomeThread();
		Thread t1 = new Thread(someThread);
		t1.setName("Thread-1");
		Thread t2 = new Thread(someThread);
		t2.setName("Thread-2");
		t1.start();
		t2.start();
	}
}

class SomeThread implements Runnable {
	public void run() {
		for (int i = 0; i < 100; i++) {
			// これは意図したとおり排他的に動く
			// mainの中でThreadをnewする時、同じSomeThreadのインスタンスを使っているので、
			// 同じロックでガードされる
			synchronizedMethod(i);
		}
	}

	private synchronized void synchronizedMethod(int i) {
		System.out.println(Thread.currentThread().getName() + " : " + i);
	}
}

SynchronizedTest2.java

public class SynchronizedTest2 {
	public static void main(String[] args) {
		SomeThread2 someThread = new SomeThread2();
		Thread t1 = new Thread(someThread);
		t1.setName("Thread-1");
		Thread t2 = new Thread(someThread);
		t2.setName("Thread-2");
		t1.start();
		t2.start();
	}
}

class SomeThread2 implements Runnable {
	public void run() {
		for (int i = 0; i < 100; i++) {
			// これは意図したとおり排他的に動かない
			// synchronizedMethod(i)はsynchronizedだが、SynchronizedMethodClassのインスタンスが
			// 異なる(毎回newしている)ので、ロックでガードされない
			new SynchronizedMethodClass().synchronizedMethod(i);
		}
	}
}

class SynchronizedMethodClass {
	synchronized void synchronizedMethod(int i) {
		System.out.println(Thread.currentThread().getName() + " : " + i);
	}
}

SynchronizedTest3.java

public class SynchronizedTest3 {
	public static void main(String[] args) {
		SomeThread3 someThread = new SomeThread3();
		Thread t1 = new Thread(someThread);
		t1.setName("Thread-1");
		Thread t2 = new Thread(someThread);
		t2.setName("Thread-2");
		t1.start();
		t2.start();
	}
}

class SomeThread3 implements Runnable {
	SynchronizedMethodClass3 smc = new SynchronizedMethodClass3();
	public void run() {
		for (int i = 0; i < 100; i++) {
			// これは意図したとおり排他的に動く
			// mainの中でThreadをnewする時、同じSomeThread3のインスタンスを使っているので、
			// 同じロックでガードされる
			smc.synchronizedMethod(i);
		}
	}
}

class SynchronizedMethodClass3 {
	synchronized void synchronizedMethod(int i) {
		System.out.println(Thread.currentThread().getName() + " : " + i);
	}
}

SynchronizedTest4.java

public class SynchronizedTest4 {
	public static void main(String[] args) {
		SomeThread4 someThread = new SomeThread4();
		SomeThread4 someThread2 = new SomeThread4();
		Thread t1 = new Thread(someThread);
		t1.setName("Thread-1");
		Thread t2 = new Thread(someThread2);
		t2.setName("Thread-2");
		t1.start();
		t2.start();
	}
}

class SomeThread4 implements Runnable {
	SynchronizedMethodClass4 smc = new SynchronizedMethodClass4();
	public void run() {
		for (int i = 0; i < 100; i++) {
			// これは意図したとおり排他的に動かない
			// mainの中でThreadをnewする時、違うSomeThread4のインスタンスを使っているので、
			// 同じロックでガードされない
			smc.synchronizedMethod(i);
		}
	}
}

class SynchronizedMethodClass4 {
	synchronized void synchronizedMethod(int i) {
		System.out.println(Thread.currentThread().getName() + " : " + i);
	}
}

スレッドセーフではないコード(2回目)

前回(1回目)サーブレットを例にスレッドセーフではないコードを考えてみたが、スレッドセーフではないことを目に見えるかたちで実証するのが難しかった。
今回はJava SEの環境で動かせるクラスでスレッドセーフではないことを実証してみる。

まずは、カウントを返すクラスを用意する。

/**
 * スレッドセーフでない順序数生成メソッド
 */
public class UnsafeSequence {
	/**
	 * カウンタ
	 */
	private int nextValue;

	/**
	 * 重複のない数を返す
	 */
	public int getNext() {
		return nextValue++;
	}
}

次に、このカウンターを使うクラスを用意する。mainを起動すると650個のスレッドが立ち上がり、各スレッドは100回カウンターを呼び出す。

public class ThreadMain {
	public static void main(String[] args) {
		UnsafeSequenceCallerThread callerThread = new UnsafeSequenceCallerThread();
		for (int i = 0; i < 650; i++) {
			Thread thread = new Thread(callerThread);
			thread.start();
		}
	}
}

class UnsafeSequenceCallerThread implements Runnable {
	UnsafeSequence unsafeSequence = new UnsafeSequence();

	public void run() {
		for (int i = 0; i < 100; i++) {
			int sequence = unsafeSequence.getNext();
			System.out.println(sequence);
		}

	}
}

mainを実行した結果はどうなっているだろうか?出力結果を降順にソートしてみてほしい。実行環境やタイミングによって異なるが、同じ数値が2度3度と出力され、一番大きい数が「64999」ではないハズだ。私の環境では「64997」だった。

では、期待通り動かすにはどうすればよいか?一番簡単なのはgetNextメソッド自体をsynchronizedにすること。これにより、nextValueにアクセスする唯一のgetNextメソッドがロックによってガードされることになる。

ちなみに、nextValueをvolatileにしてもダメ。nextValue++の++が非アトミックな操作なので。

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

スレッドセーフではないコード(3回目)

他人が書いたコードがマルチスレッド下で正しく動くかどうか検証するのは骨の折れる作業と実感する。

テスト時になるべくバグが出やすくするコツとしてClient VMではなくServer VMで動かすとよいそうだ。ServerVMの方がClientVMにくらべてより最適化しようとする。

Eclipseで実行する場合は下図のようにVM起動引数に-serverを指定する。

また、実際にServerVMで起動したことを確認するには、環境変数「_JAVA_LAUNCHER_DEBUG」を設定する。値は空白以外なら何でも良いようだ。

この状態で実行すると、実行後に以下のように情報が出力される。

----_JAVA_LAUNCHER_DEBUG----
(途中省略)
JVM path is C:\root\usr\local\pleiades-3.7.2\eclipse\jre\bin\server\jvm.dll
JRE path is C:\root\usr\local\pleiades-3.7.2\eclipse\jre
CRT path is C:\root\usr\local\pleiades-3.7.2\eclipse\jre\bin\msvcr71.dll
(途中省略)
----_JAVA_LAUNCHER_DEBUG----

sever\jvm.dll と出力されればServerVMで起動したことを確認できる。

スレッドセーフではないコード(1回目)

書いてはいけないコードの例。アクセスカウント(requestCount)を表示するサーブレットを考える。

サーブレット自体はシングルトンなので、ブラウザからアクセスするとrequestCountの値がインクリメントされ、正しく動くかのように見える。

しかし、++のインクリメントはアトミックな操作ではないので、正しくインクリメントされた値がブラウザに表示される保証は無い。これを「競り合い状態(race condition)」と言う*1

仕様は理解できるが、実際に期待通り動作しない状況を再現するにはどうすれば良いものやら・・・次回を参考にして下さい。

package test;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class CounterServlet extends HttpServlet {

	int requestCount = 0;

	public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
		PrintWriter writer = response.getWriter();
		writer.print(++requestCount);
		writer.close();
	}
}

サーブレットの仕様に関する参考
http://stackoverflow.com/questions/10665223/why-apache-servlet-is-singleton

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

*1:Java並行処理プログラミングのp6〜p9参照

Sambaとネットワーク設定

Ubuntuの初期設定では、NATのポートフォワーディングを利用してホストOSのWindows7からゲストOSのUbuntuSSH接続した。また、WindowsUbuntuのファイル共有はVirtualBoxの共有フォルダーを利用した。

VirtualBoxのファイル共有だけでは今後何かと不便だろうと思い、Sambaをインストールすることに。

$ sudo apt-get install samba

Sambaのインストール自体はこれで完了。まずはダメもとでWindowsからUbuntu(\\192.168.56.1)につないでみると・・なぜか、Windows側PCの共有フォルダーが見えている。ポートフォワーディングを設定していないので、単純にどこにもつながらないエラーが出るのかと思っていたが、そうではないらしい。良くわからない。。
どちらにせよダメなので、VirtualBox Virtual Networkingを参考にネットワーク設定を再考する。

[要件]
1) ゲストOSのUbuntuからもホストOSのWindowsを通じてインターネットに接続したい
2) ゲストOSのUbuntuはホストOSのWindows以外のPCからは見えてほしくない
3) ホストOSのWindowsからゲストOSのUbuntuには無制限にアクセス可能

1)と2)の要件はUbuntuの初期設定のようにNATで問題なし。
3)の要件を満たすため、アダプター2に「ホストオンリーアダプター」を割り当てる。VirtualBox側の設定はこれで完了。

次はUbuntu側の設定。上で割り当てたネットワークは自動起動しないので、手動でIPを割り当てる。

$ sudo dhclient eth1
[sudo] password for mokimokisan: 
Rather than invoking init scripts through /etc/init.d, use the service(8)
utility, e.g. service smbd reload

Since the script you are attempting to invoke has been converted to an
Upstart job, you may also use the reload(8) utility, e.g. reload smbd

ifconfig で割り当てられたIPアドレスを確認する。
毎回dhclientコマンドを打つのはさすがに面倒なので、Ubuntuのネットワーク設定に追加する。

$ cat /etc/network/interfaces
# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto eth0
iface eth0 inet dhcp

# VirtualBox Host-only Network
auto eth1
iface eth1 inet dhcp

上記はDHCPIPアドレスを取得する設定だが、DHCPで割り当てるIPアドレスの範囲はVirtualBox側の [ファイル] > [環境設定] > [ネットワーク] で設定可能。

Windows7に仮想環境を作ってUbuntuをインストールする(デスクトップEditionとサーバーEdition の両方)

前々からcoLinuxWindowsLinuxを共存させたいと考えていたが、いざやってみるとcoLinuxが64bitのWindowsに対応していないことが判明。そこで、VirtualBoxを使って仮想環境を作成してUbuntuのインストールにトライする。

仮想環境 : VirtualBox 4.2.6 for Windows hosts
ホストOS : Windows7(64bit)
ゲストOS : Ubuntu12.10(64bit)

[デスクトップEditionインストール手順]
1) VirtualBoxをダウンロードしてインストールする。
2) VirtualBoxを起動して新規仮想マシンを作成する。http://pedantic.jp/?siid=ubuntu_virtualboxhttp://zakkiweb.net/ubuntu/1204/0.shtml を参考にすればよい。ポイントはダウンロードしたUbuntuのISOイメージをVirtualBoxの設定の ストレージ > IDEセカンダリマスター に設定するところ。
3) VirtualBox仮想マシンの電源をONにするとUbuntuのインストール画面が起動するのでウイザードに従ってインストールする。


昔は身近にフリーの仮想環境も無かったのでデュアルブートさせるのが普通だったが、便利な時代になったものだと身にしみる。


[デスクトップEditionの追加手順]
4) Guest Additions のインストー
画面の解像度を増やす場合や、ホストOS−ゲストOS間で共有フォルダを作る場合に必要。


インストール後にCUI起動に変更しようとするが、RedHat系と仕組みが異なっていて勝手が全く分からない...
しかもUbuntuのDesktopエディションを選択したがゆえ、リモートからのアクセスに必要なsshすらインストールされていない。
色々インストールしたり、RedHat系で言うランレベルを変更したりといった作業も面倒なので、Serverエディションでインストールし直そう。


[サーバーEditionのインストールメモ]
1) ubuntu-12.10-server-amd64.iso をダウンロードしてVirtualBoxの ストレージ > IDE に設定。
2) ここが重要。下図の通り VirtualBoxの「ポートフォワーディング ルール」を設定する。sshで接続したいので22番ポート。

3) VirtualBox仮想マシンの電源をONにしてウイザードに従ってインストールする。リモート(Windows7のホストOS)からsshで接続して操作するために、追加ソフトウェアの選択で「OpenSSH server」を選択してインストールする。
4) インストールが終わるとUbuntuが再起動される。

Windows側からPuTTYPoderosaで接続できれば、初期設定はひとまず完了。


[サーバーEditionの追加手順(UbuntuWindowsとのファイル共有)]
Installing VirtualBox Guest Additions on Ubuntu Serverを参考に、下記コマンドを順に実行してVirtualBoxのGuest Additionsをインストールする。

$ sudo mount /dev/cdrom /media/cdrom
$ sudo apt-get install -y dkms build-essential linux-headers-generic linux-headers-$(uname -r)
$ sudo /media/cdrom/VBoxLinuxAdditions.run

sudo /media/cdrom/VBoxLinuxAdditions.run で

Installing the Window System drivers ...fail!
(Could not find the X.Org or XFree86 Window System.)

というエラーが出るが、GUIはインストールしていないので問題なし。VirtualBoxの デバイス > 共有フォルダ を開いて共有フォルダを追加する。

マウントする。

$ sudo mount -t vboxsf ubuntu-12.10-server-amd64.tmp /mnt

こんな感じでファイル共有できている。日本語のファイルでも文字化けしていないことに感激。

mokimokisan@ubuntu-12:/mnt$ touch test.txt
mokimokisan@ubuntu-12:/mnt$ ls
test.txt  新規 Microsoft Word 文書.doc
mokimokisan@ubuntu-12:/mnt$ ls -al
合計 19
drwxrwxrwx  1 root root     0  121 23:28 .
drwxr-xr-x 23 root root  4096  120 22:12 ..
-rwxrwxrwx  1 root root     0  121 23:28 test.txt
-rwxrwxrwx  1 root root 15360  121 23:27 新規 Microsoft Word 文書.doc
mokimokisan@ubuntu-12:/mnt$ 


[その他]
インストール済ソフトウェアの一覧を確認するコマンドは

$ dpkg -l

新しくソフトウェアをインストールするには、まず

$ sudo apt-get update

でデータベースを最新化。

次にインストールしたいソフトウェアを検索する。

$ apt-cache search [パッケージ名]

今さらながらStruts1系のActionやFormをJUnitでテストする方法

StrutsのActionやFormをJUnitでテストするには StrutsTestCase http://strutstestcase.sourceforge.net/ を使う。

設定ファイルを置く場所は厄介。次のようにマニュアル通り記述しても

	public void setUp() throws Exception {
		super.setUp();

		// コンテキストパスの指定
		setContextDirectory(new java.io.File("."));
		// Servletコンフィグファイル(web.xml)の指定
		setServletConfigFile("/src/main/webapp/WEB-INF/web.xml");
		// Strutsコンフィグファイルの指定
		setConfigFile("/src/main/webapp/WEB-INF/struts-config.xml");
	}

下記のようにエラーが出る。

致命的: Error initializing action servlet
javax.servlet.UnavailableException: /WEB-INF/web.xmlが見つかりません

解決策が浮かばない。テストを実行する時だけ /src/main/webapp/WEB-INF/web.xml を /WEB-INF/web.xml にコピーするとひとまずは動くようになる。

簡単なテストコードはこんな感じか↓

	@Test
	public final void testValidateActionMappingHttpServletRequest() {
		setRequestPathInfo("/helloWorld.do");
		addRequestParameter("country", "FR");
		actionPerform();
		verifyNoActionErrors();
		verifyForward("success");
	}


ActionFormのvalidateが呼ばれるかどうかはstruts-config.xmlの設定次第。
validate=trueなら呼ばれるし、falseなら呼ばれない。

	<action-mappings>
		<action path="/helloWorld" type="com.mkyong.common.action.HelloWorldAction"
			name="helloWorldForm" validate="true">
			<forward name="success" path="/HelloWorld.jsp" />
		</action>
	</action-mappings>

※参考にしたサイト
Test-Driven Development Using StrutsTestCase http://onjava.com/pub/a/onjava/2005/10/26/test-driven-development-using-strutstestcase.html
Struts Hello World Example http://www.mkyong.com/struts/struts-hello-world-example/

Mavenのディレクトリ構成を変更(+Mockitoを使ってサーブレットをJUnitでテスト)する方法

Mavenで決まっているディレクトリの構成を変えるのは公式サイトを見ても「推奨しない」だの「考え直せ」だの書かれているが、それでも変更しなければならない状況におちいることがあろうかと思います。例えば下図のような構成のプロジェクトで試しにMavenを使ってみたい、とか。

解決方法は幾つかありますが pom.xml だけの変更で済ませたい場合はソースがあるディレクトリを指定し、クラスパスを全て設定するしか無いのではないか。
Antみたいにフォルダ内のjarを * 指定できれば楽なのだが...
また、compile用とtest-compile用のクラスパスは別々に設定する必要があります。

<build>
	<sourceDirectory>src</sourceDirectory>
	<testSourceDirectory>test</testSourceDirectory>
	<plugins>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-compiler-plugin</artifactId>
			<version>3.0</version>
			<configuration>
				<source>1.6</source>
				<target>1.6</target>
				<compilerArguments>
					<verbose />
					<!-- ここでclasspathを指定するとそれまでにmavenが設定したclasspathを上書きするので注意すること -->
					<classpath>
						src;test;target/classes;WebContent/WEB-INF/lib/servlet-api.jar;WebContent/WEB-INF/lib/junit-4.11.jar;WebContent/WEB-INF/lib/mockito-all-1.9.5.jar
					</classpath>
				</compilerArguments>
			</configuration>
		</plugin>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-surefire-plugin</artifactId>
			<version>2.13</version>
			<configuration>
				<additionalClasspathElements>
					<additionalClasspathElement>WebContent/WEB-INF/lib/servlet-api.jar</additionalClasspathElement>
					<additionalClasspathElement>WebContent/WEB-INF/lib/junit-4.11.jar</additionalClasspathElement>
					<additionalClasspathElement>WebContent/WEB-INF/lib/mockito-all-1.9.5.jar</additionalClasspathElement>
				</additionalClasspathElements>
			</configuration>
		</plugin>
	</plugins>
</build>

テスト可能なファイルは 20130104_SonarSettingTest.zip 直 からダウンロードでき、

> mvn clean compile test

で Mockito を使ったサーブレットのテストまでを実行できます。

コンパイルが通らないとかいった場合は -X オプションを付けて実行するとMavenの挙動が詳細に出力されるので、何かの手がかりになるでしょう。

> mvn -X clean compile test

Jenkinsの使い方+MavenやSonarとの連携

Jenkinsの導入〜MavenSonarとの連携方法までを紹介します。SubversionやGitとの連携は次回以降に紹介したいと思います。

まずはJenkinsの準備。
http://jenkins-ci.org/ からWARファイルをダウンロードし、javaコマンドでサーバを起動します。

C:\root\usr\local>java -jar jenkins.war
Running from: C:\root\usr\local\jenkins.war
webroot: $user.home/.jenkins
1 03, 2013 11:01:01 午後 winstone.Logger logInternal
情報: Beginning extraction from war file
Jenkins home directory: C:\Users\XXXXXXXXX\.jenkins found at: $user.home/.jenkins
1 03, 2013 11:01:12 午後 winstone.Logger logInternal
情報: HTTP Listener started: port=8080
1 03, 2013 11:01:12 午後 winstone.Logger logInternal
情報: AJP13 Listener started: port=8009
1 03, 2013 11:01:12 午後 winstone.Logger logInternal
情報: Winstone Servlet Engine v0.9.10 running: controlPort=disabled
1 03, 2013 11:01:13 午後 jenkins.InitReactorRunner$1 onAttained
情報: Started initialization
1 03, 2013 11:01:19 午後 jenkins.InitReactorRunner$1 onAttained
情報: Listed all plugins
1 03, 2013 11:01:19 午後 jenkins.InitReactorRunner$1 onAttained
情報: Prepared all plugins
1 03, 2013 11:01:19 午後 jenkins.InitReactorRunner$1 onAttained
情報: Started all plugins
1 03, 2013 11:01:19 午後 jenkins.InitReactorRunner$1 onAttained
情報: Augmented all extensions
1 03, 2013 11:01:19 午後 jenkins.InitReactorRunner$1 onAttained
情報: Loaded all jobs
1 03, 2013 11:01:22 午後 org.apache.sshd.common.util.SecurityUtils$BouncyCastleRegistration run
情報: Trying to register BouncyCastle as a JCE provider
1 03, 2013 11:01:23 午後 org.apache.sshd.common.util.SecurityUtils$BouncyCastleRegistration run
情報: Registration succeeded
1 03, 2013 11:01:23 午後 org.jenkinsci.main.modules.sshd.SSHD start
情報: Started SSHD at port 49464
1 03, 2013 11:01:23 午後 jenkins.InitReactorRunner$1 onAttained
情報: Completed initialization
1 03, 2013 11:01:23 午後 hudson.TcpSlaveAgentListener <init>
情報: JNLP slave agent listener started on TCP port 49465
1 03, 2013 11:01:24 午後 hudson.WebAppMain$2 run
情報: Jenkins is fully up and running

ブラウザから http://localhost:8080/ にアクセスすると下記のような画面が表示されます。

次はMavenでのビルドですが、Sonarとの連携も考慮し、まずはSonarを起動しておきます(Sonarについての詳細は http://d.hatena.ne.jp/mokimokisan/20130101/1357055791 を参照)。

c:\root\usr\local\sonar-3.4\bin\windows-x86-64>StartSonar.bat
wrapper  | --> Wrapper Started as Console
wrapper  | Launching a JVM...
jvm 1    | Wrapper (Version 3.2.3) http://wrapper.tanukisoftware.org
jvm 1    |   Copyright 1999-2006 Tanuki Software, Inc.  All Rights Reserved.
jvm 1    |
jvm 1    | 2013-01-03 23:17:08.340:INFO::Logging to org.sonar.application.FilteredLogger@339cefd6 via org.sonar.application.FilteredLogger
jvm 1    | 2013-01-03 23:17:08.411:INFO::jetty-6.1.25
jvm 1    | 2013-01-03 23:17:09.445:INFO::NO JSP Support for /, did not find org.apache.jasper.servlet.JspServlet
jvm 1    | JRuby limited openssl loaded. http://jruby.org/openssl
jvm 1    | gem install jruby-openssl for full support.
jvm 1    | 2013-01-03 23:17:57.365:INFO::Started SelectChannelConnector@0.0.0.0:9000

下準備ができたので、JenkinsにMavenでのビルドジョブを登録します。
まずは、新規ジョブ作成から [Maven2/3プロジェクトのビルド] を選択します。

※ビルドするプロジェクトは http://d.hatena.ne.jp/mokimokisan/20130101/1357055791 で使ったプロジェクトと同じものです。
ファイルは
20121228_SpringTest.zip 直
からダウンロードできます。

次画面で「Maven2/3のインストール先を設定する必要があります。」と表示されるので [システムの設定] をクリックします。

JDKMavenのインストーディレクトリを指定し、画面左下の [保存] をクリックします。

ビルドの設定では、試しに前処理として clean install -DskipTests=true
ビルドとして sonar:sonar を指定し、画面左下の [保存] をクリックします。
※ビルドの箇所に clean install -DskipTests=true sonar:sonar と記述すると sonar:sonar でもテストが省略されてしまいます。

ここまでで設定完了したのでビルドを実行します。

ビルドが成功すると下図のようになります。失敗した場合はコンソール出力のログから原因を探って下さい。

Sonar http://localhost:9000/ にアクセスしてTime Machineをみると、Jenkinsから実行されたテストのメトリクスが収集されてSonarに登録されていることが分かります。

SonarとMavenでJavaソースコードのメトリクスを収集する

Javaソースコードカバレッジツールを探していたらSonarというJavaソースコードのメトリクス収集ツールに行き当たりました。
これまでソースをWebで見れるようにするツール等は様々出回っていましたが、総合的にメトリクスを収集してくれるツールは、多くは無かったのではないかと思います。
カバレッジのみならず、CheckstyleやPMD等も自動的に実行されとても便利です。どんなツールか感じを掴むとっかかりとしてご覧ください。
JavaMavenはインストールされている前提です。
※コマンド等は Windows7 64bit 環境での実行例です。

・まずはSonarのインストー
http://www.sonarsource.org/downloads/ から sonar-3.4.zip をダウンロード、適当なディレクトリに解凍する。

Sonarを起動する
sonar-3.4/bin/windows-x86-64/StartSonar.bat を実行する。

wrapper  | --> Wrapper Started as Console
wrapper  | Launching a JVM...
jvm 1    | Wrapper (Version 3.2.3) http://wrapper.tanukisoftware.org
jvm 1    |   Copyright 1999-2006 Tanuki Software, Inc.  All Rights Reserved.
jvm 1    |
jvm 1    | 2013-01-01 23:41:22.836:INFO::Logging to org.sonar.application.FilteredLogger@339cefd6 via org.sonar.application.FilteredLogger
jvm 1    | 2013-01-01 23:41:22.967:INFO::jetty-6.1.25
jvm 1    | 2013-01-01 23:41:23.502:INFO::NO JSP Support for /, did not find org.apache.jasper.servlet.JspServlet
jvm 1    | JRuby limited openssl loaded. http://jruby.org/openssl
jvm 1    | gem install jruby-openssl for full support.
jvm 1    | 2013-01-01 23:41:55.976:INFO::Started SelectChannelConnector@0.0.0.0:9000

http://localhost:9000 にアクセスして下記のような画面が表示されればSonarの起動完了。


Mavenの設定
特に無し。下記のコマンドを順に実行する。

mvn clean install -DskipTests=true
mvn sonar:sonar

実行ログを参考までに載せておきます。
ソースコードhttp://d.hatena.ne.jp/mokimokisan/20121227/1356624640 に掲載した 20121228_SpringTest.zip 直 を使いました。

c:\SpringTest>mvn clean install -DskipTests=true
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building SpringTest 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.4.1:clean (default-clean) @ SpringTest ---
[INFO] Deleting c:\SpringTest\target
[INFO]
[INFO] --- maven-resources-plugin:2.4.3:resources (default-resources) @ SpringTest ---
[WARNING] Using platform encoding (MS932 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 1 resource
[INFO]
[INFO] --- maven-compiler-plugin:2.3.2:compile (default-compile) @ SpringTest ---
[WARNING] File encoding has not been set, using platform encoding MS932, i.e. build is platform dependent!
[INFO] Compiling 6 source files to c:\SpringTest\target\classes
[INFO]
[INFO] --- maven-resources-plugin:2.4.3:testResources (default-testResources) @ SpringTest ---
[WARNING] Using platform encoding (MS932 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 0 resource
[INFO]
[INFO] --- maven-compiler-plugin:2.3.2:testCompile (default-testCompile) @ SpringTest ---
[WARNING] File encoding has not been set, using platform encoding MS932, i.e. build is platform dependent!
[INFO] Compiling 1 source file to c:\SpringTest\target\test-classes
[INFO]
[INFO] --- maven-surefire-plugin:2.7.2:test (default-test) @ SpringTest ---
[INFO] Tests are skipped.
[INFO]
[INFO] --- maven-jar-plugin:2.3.1:jar (default-jar) @ SpringTest ---
[INFO] Building jar: c:\SpringTest\target\SpringTest-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- maven-install-plugin:2.3.1:install (default-install) @ SpringTest ---
[INFO] Installing c:\SpringTest\target\SpringTest-0.0.1-SNAPSHOT.jar to C:\Users\XXXXXXXXX\.m2\repository\SpringTest\SpringTest\0.0.1-SNAPSHOT\SpringTest-0.0.1-SNAPSHOT.jar
[INFO] Installing c:\SpringTest\pom.xml to C:\Users\XXXXXXXXX\.m2\repository\SpringTest\SpringTest\0.0.1-SNAPSHOT\SpringTest-0.0.1-SNAPSHOT.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4.017s
[INFO] Finished at: Wed Jan 02 00:10:42 JST 2013
[INFO] Final Memory: 15M/110M
[INFO] ------------------------------------------------------------------------
c:\SpringTest>mvn sonar:sonar
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building SpringTest 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- sonar-maven-plugin:2.0:sonar (default-cli) @ SpringTest ---
[INFO] Sonar version: 3.4
[INFO] [00:13:32.427] Load project settings
[INFO] [00:13:32.494] Install plugins
[INFO] [00:13:34.169] Apply project exclusions
[INFO] [00:13:34.171] Install JDBC driver
[WARN] [00:13:34.208] H2 database should be used for evaluation purpose only
[INFO] [00:13:34.210] Create JDBC datasource for jdbc:h2:tcp://localhost/sonar
[INFO] [00:13:34.354] Initializing Hibernate
[INFO] [00:13:37.468] -------------  Analyzing SpringTest
[INFO] [00:13:37.474] Load module settings
[INFO] [00:13:38.740] Quality profile : [name=Sonar way,language=java]
[INFO] [00:13:38.795] Configure maven plugins...
[INFO] [00:13:38.956] Compare to previous analysis
[INFO] [00:13:39.068] Compare over 5 days (2012-12-28)
[INFO] [00:13:39.130] Compare over 30 days (2012-12-03)
[INFO] [00:13:39.324] JaCoCo agent (version 0.5.10.201208310627) extracted: C:\Users\XXXXXX~1\AppData\Local\Temp\jacocoagent6536198146564021596.jar
[INFO] [00:13:39.326] JVM options: -javaagent:C:\Users\XXXXXX~1\AppData\Local\Temp\jacocoagent6536198146564021596.jar=destfile=target/jacoco.exec,excludes=*_javassist_*
[INFO] [00:13:39.365] Initializer FindbugsMavenInitializer...
[INFO] [00:13:39.405] Initializer FindbugsMavenInitializer done: 40 ms
[INFO] [00:13:39.406] Execute maven plugin maven-surefire-plugin...
[INFO] [00:13:39.409] Execute org.apache.maven.plugins:maven-surefire-plugin:2.7.2:test...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building SpringTest 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-surefire-plugin:2.7.2:test (default-cli) @ SpringTest ---
[INFO] Surefire report directory: c:\SpringTest\target\surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running sample.di.business.service.ProductServiceTest
1 02, 2013 12:13:40 午前 org.springframework.test.context.TestContextManager retrieveTestExecutionListeners
情報: @TestExecutionListeners is not present for class [class sample.di.business.service.ProductServiceTest]: using defaults.
1 02, 2013 12:13:41 午前 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
情報: Loading XML bean definitions from class path resource [applicationContext.xml]
1 02, 2013 12:13:41 午前 org.springframework.context.support.AbstractApplicationContext prepareRefresh
情報: Refreshing org.springframework.context.support.GenericApplicationContext@1ed6ecc2: display name [org.springframework.context.support.GenericApplicationContext@1ed6ecc2]; startup date [Wed Jan 02 00:13:41 JST 2013]; root of context hierarchy
1 02, 2013 12:13:41 午前 org.springframework.context.support.AbstractApplicationContext obtainFreshBeanFactory
情報: Bean factory for application context [org.springframework.context.support.GenericApplicationContext@1ed6ecc2]: org.springframework.beans.factory.support.DefaultListableBeanFactory@7afb34d1
1 02, 2013 12:13:41 午前 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
情報: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@7afb34d1: defining beans [org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,ProductService,productDaoImpl]; root of factory hierarchy
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.351 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
1 02, 2013 12:13:41 午前 org.springframework.context.support.AbstractApplicationContext doClose
情報: Closing org.springframework.context.support.GenericApplicationContext@1ed6ecc2: display name [org.springframework.context.support.GenericApplicationContext@1ed6ecc2
]; startup date [Wed Jan 02 00:13:41 JST 2013]; root of context hierarchy

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 12.689s
[INFO] Finished at: Wed Jan 02 00:13:42 JST 2013
[INFO] Final Memory: 25M/300M
[INFO] ------------------------------------------------------------------------
[INFO] [00:13:42.315] Execute org.apache.maven.plugins:maven-surefire-plugin:2.7.2:test done: 2906 ms
[INFO] [00:13:42.317] Execute maven plugin maven-surefire-plugin done: 2911 ms
[INFO] [00:13:42.320] Initializer JacocoMavenInitializer...
[INFO] [00:13:42.322] Initializer JacocoMavenInitializer done: 2 ms
[INFO] [00:13:42.324] Initializer ProjectFileSystemLogger...
[INFO] [00:13:42.327] Excluded tests: [**/package-info.java]
[INFO] [00:13:42.329] Source directories:
[INFO] [00:13:42.331]   c:\SpringTest\src\main\java
[INFO] [00:13:42.334] Test directories:
[INFO] [00:13:42.336]   c:\SpringTest\src\test\java
[INFO] [00:13:42.339] Initializer ProjectFileSystemLogger done: 15 ms
[INFO] [00:13:42.341] Initializer MavenInitializer...
[INFO] [00:13:42.348] Java source version: 1.5
[INFO] [00:13:42.350] Java target version: 1.5
[INFO] [00:13:42.352] Source encoding: null
[INFO] [00:13:42.354] Initializer MavenInitializer done: 13 ms
[INFO] [00:13:42.370] Sensor JavaSourceImporter...
[INFO] [00:13:42.577] Sensor JavaSourceImporter done: 207 ms
[INFO] [00:13:42.578] Sensor JavaSquidSensor...
[INFO] [00:13:42.667] Java AST scan...
[INFO] [00:13:42.802] Java AST scan done: 135 ms
[INFO] [00:13:42.808] Java bytecode scan...
[INFO] [00:13:42.872] Java bytecode scan done: 64 ms
[INFO] [00:13:42.892] Package design analysis...
[INFO] [00:13:42.919] Package design analysis done: 27 ms
[INFO] [00:13:42.938] Sensor JavaSquidSensor done: 360 ms
[INFO] [00:13:42.940] Sensor JaCoCoSensor...
[INFO] [00:13:42.947] Analysing C:\SpringTest\target\jacoco.exec
[INFO] [00:13:43.069] Sensor JaCoCoSensor done: 129 ms
[INFO] [00:13:43.070] Sensor CpdSensor...
[INFO] [00:13:43.072] SonarEngine is used
[INFO] [00:13:43.079] Cross-project analysis disabled
[INFO] [00:13:43.142] Sensor CpdSensor done: 72 ms
[INFO] [00:13:43.144] Sensor CheckstyleSensor...
[INFO] [00:13:43.149] Execute Checkstyle 5.6...
[INFO] [00:13:43.183] Checkstyle configuration: c:\SpringTest\target\sonar\checkstyle.xml
[INFO] [00:13:43.973] Execute Checkstyle 5.6 done: 824 ms
[INFO] [00:13:43.983] Sensor CheckstyleSensor done: 839 ms
[INFO] [00:13:43.984] Sensor PmdSensor...
[INFO] [00:13:43.990] Execute PMD 4.3...
[INFO] [00:13:44.010] Java version: 1.5
[INFO] [00:13:44.053] PMD configuration: c:\SpringTest\target\sonar\pmd.xml
[INFO] [00:13:45.642] PMD configuration: c:\SpringTest\target\sonar\pmd-unit-tests.xml
[INFO] [00:13:45.647] Execute PMD 4.3 done: 1657 ms
[INFO] [00:13:45.700] Sensor PmdSensor done: 1716 ms
[INFO] [00:13:45.701] Sensor ProfileSensor...
[INFO] [00:13:45.978] Sensor ProfileSensor done: 277 ms
[INFO] [00:13:45.979] Sensor ProfileEventsSensor...
[INFO] [00:13:46.007] Sensor ProfileEventsSensor done: 28 ms
[INFO] [00:13:46.008] Sensor ProjectLinksSensor...
[INFO] [00:13:46.016] Sensor ProjectLinksSensor done: 8 ms
[INFO] [00:13:46.019] Sensor VersionEventsSensor...
[INFO] [00:13:46.030] Sensor VersionEventsSensor done: 11 ms
[INFO] [00:13:46.033] Sensor Maven dependencies...
[INFO] [00:13:46.229] Sensor Maven dependencies done: 196 ms
[INFO] [00:13:46.231] Sensor SurefireSensor...
[INFO] [00:13:46.236] parsing c:\SpringTest\target\surefire-reports
[INFO] [00:13:46.360] Sensor SurefireSensor done: 129 ms
[INFO] [00:13:46.806] Execute decorators...
[INFO] [00:13:47.990] ANALYSIS SUCCESSFUL, you can browse http://localhost:9000
[INFO] [00:13:47.994] Executing post-job class org.sonar.plugins.core.batch.IndexProjectPostJob
[INFO] [00:13:48.112] Executing post-job class org.sonar.plugins.dbcleaner.ProjectPurgePostJob
[INFO] [00:13:48.133] -> Keep one snapshot per day between 2012-12-05 and 2013-01-01
[INFO] [00:13:48.138] -> Keep one snapshot per week between 2012-01-04 and 2012-12-05
[INFO] [00:13:48.140] -> Keep one snapshot per month between 2008-01-09 and 2012-01-04
[INFO] [00:13:48.143] -> Delete data prior to: 2008-01-09
[INFO] [00:13:48.150] -> Clean SpringTest [id=1]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 18.969s
[INFO] Finished at: Wed Jan 02 00:13:48 JST 2013
[INFO] Final Memory: 21M/359M
[INFO] ------------------------------------------------------------------------
c:\SpringTest>

あらためて http://localhost:9000 にアクセスすると、下記のような画面が表示され、Sonarによってメトリクスが収集されたことが分かります。

個々のソースコードカバレッジ等もグラフィカルに表示されています。

SpringのDI(Dependency Injection)のサンプルを作りました

Springフレームワークが提供するDI(Dependency Injection)を理解するためのサンプルプログラムを作りました。
『Spring3入門』の 第2章 SpringのDI を参考にしています。初版の第一刷を買ったせいか、それじゃ動かないだろ、というサンプルソースもあるが許容範囲内。Java分かるけどSpring初めての人には為になる印象です。

Spring3入門 ――Javaフレームワーク・より良い設計とアーキテクチャ

Spring3入門 ――Javaフレームワーク・より良い設計とアーキテクチャ

DIの概念などは書籍や他のサイトの説明に譲るとして、ここでは初めてSpringを動かした感想のみ。

JavaSE1.5以上のバージョンでの実開発経験が無い身にとって衝撃的だったのは、DAOを呼び出す時に実装クラスをnewする必要が無いところ。
しかもDAOは呼び出しの都度Springがnewするのではなく、Singletonのインスタンスとして取得できる(つまりSpringからinjectされる)。
DAOをSingletonにする理由は明確でないが、都度newしないのでパフォーマンス向上は期待できると思われる。
※誤解の無いように言うと、都度newされた(Singletonでない)インスタンスとして取得することも可能。

また、ビジネスロジックの開発者がDAOの実装クラスをnewする必要なくインスタンスを取得できるので、DAOの呼び出し側(ビジネスロジック層)にDAOの実装クラス名は一切出てこない。
これは本当に気持ちが良い。インタフェースによるプログラムが可能になったのだと実感が沸く。

JUnitのテストコードの書き方に若干コツが必要なので、サンプルプログラムのソースコードをダウンロードできるようにしておきます。
20121228_SpringTest.zip 直

Eclipseで実行する場合は、一旦Eclipseのプロジェクトに変換してからインポートするのが楽です。

c:\SpringTest>mvn eclipse:eclipse
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building SpringTest 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] >>> maven-eclipse-plugin:2.8:eclipse (default-cli) @ SpringTest >>>
[INFO]
[INFO] <<< maven-eclipse-plugin:2.8:eclipse (default-cli) @ SpringTest <<<
[INFO]
[INFO] --- maven-eclipse-plugin:2.8:eclipse (default-cli) @ SpringTest ---
[INFO] Using Eclipse Workspace: null
[INFO] Adding default classpath container: org.eclipse.jdt.launching.JRE_CONTAINER
[INFO] Not writing settings - defaults suffice
[INFO] Wrote Eclipse project for "SpringTest" to c:\SpringTest.
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.809s
[INFO] Finished at: Fri Dec 28 23:15:19 JST 2012
[INFO] Final Memory: 9M/109M
[INFO] ------------------------------------------------------------------------
c:\SpringTest>


コマンドラインMavenから起動したJUnitのテスト結果も参考までに載せておきます。

c:\SpringTest>mvn test
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building SpringTest 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.4.3:resources (default-resources) @ SpringTest ---
[WARNING] Using platform encoding (MS932 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 1 resource
[INFO]
[INFO] --- maven-compiler-plugin:2.3.2:compile (default-compile) @ SpringTest ---
[WARNING] File encoding has not been set, using platform encoding MS932, i.e. build is platform dependent!
[INFO] Compiling 6 source files to c:\SpringTest\target\classes
[INFO]
[INFO] --- maven-resources-plugin:2.4.3:testResources (default-testResources) @ SpringTest ---
[WARNING] Using platform encoding (MS932 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 0 resource
[INFO]
[INFO] --- maven-compiler-plugin:2.3.2:testCompile (default-testCompile) @ SpringTest ---
[WARNING] File encoding has not been set, using platform encoding MS932, i.e. build is platform dependent!
[INFO] Compiling 1 source file to c:\SpringTest\target\test-classes
[INFO]
[INFO] --- maven-surefire-plugin:2.7.2:test (default-test) @ SpringTest ---
[INFO] Surefire report directory: c:\SpringTest\target\surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running sample.di.business.service.ProductServiceTest
12 28, 2012 12:25:26 午前 org.springframework.test.context.TestContextManager retrieveTestExecutionListeners
情報: @TestExecutionListeners is not present for class [class sample.di.business.service.ProductServiceTest]: using defaults.
12 28, 2012 12:25:26 午前 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
情報: Loading XML bean definitions from class path resource [applicationContext.xml]
12 28, 2012 12:25:27 午前 org.springframework.context.support.AbstractApplicationContext prepareRefresh
情報: Refreshing org.springframework.context.support.GenericApplicationContext@15b96350: display name [org.springframework.context.support.GenericApplicationContext@15b96350]; startup date [Fri Dec 28 00:25:27 JST 2012]; root of context hierarchy
12 28, 2012 12:25:27 午前 org.springframework.context.support.AbstractApplicationContext obtainFreshBeanFactory
情報: Bean factory for application context [org.springframework.context.support.GenericApplicationContext@15b96350]: org.springframework.beans.factory.support.DefaultListableBeanFactory@a7393a2
12 28, 2012 12:25:27 午前 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
情報: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@a7393a2: defining beans  org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,ProductService,productDaoImpl]; root of factory hierarchy
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.619 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.630s
[INFO] Finished at: Fri Dec 28 00:25:27 JST 2012
[INFO] Final Memory: 14M/110M
[INFO] ------------------------------------------------------------------------
c:\SpringTest>