久々にJavaでコーディングをする用があったのだけど、テストをJUnitで書いていたらなんだか人生が辛くなってきた。なぜ辛くなったのかは分からない。嫌な思い出でもあるのかもしれない。
そこでEclipse+Mavenの開発環境でJava開発をしつつ、テストコードだけScalaTestで書く環境を整えて、辛い過去と決別することを試みてみた。
ScalaTestやSpecsを使えば、当社比50%くらいはテストコードがすっきりし、人生の辛さの5%くらいは軽減できるかもしれない。
JavaとMavenが入ってること
Eclipse入れるところからスタート。
これを書いている時点ではScalaのプラグインは4.2には対応していない。対応してても4.x系はまだ待ちだと思われる。
なので今回は下記サイトからIndigo(Eclipse3.7)を落とす。
Download
http://www.eclipse.org/downloads/packages/release/indigo/sr2
Java開発を想定しているので、Eclipse IDE for Java Developersを選択。3.7のJava Developersはあらかじめm2eが入ってるので導入が少し楽。
落とすパッケージの違いについては下記参照
http://www.eclipse.org/downloads/compare.php
ダウンロードしたら解凍して、配下のeclipse.iniの-Xmxの指定を増やしておく。我が家では1024Mにした。少ないとScalaプラグインが駄々をこねることがある。
EclipseのMavenプラグイン、m2eを入れる。既に入っていたらこの項はスキップ。
Eclipseを立ち上げ、メニューバーから以下の操作を行う。
Help → Install New Software... → Work withで「Indigo - http://download.eclipse.org/releases/indigo」を選択 Pending...と出るのでしばし待つ(この処理には数分かかる場合があります) → Pendingが終わって選択可能なプラグインが表示される Collaboration → 「m2e - Maven Integration for Eclipse」をチェック → Next Next → Accept → Finish → インストールが始まる → 再起動する聞かれるので再起動する
これでm2eのインストール完了。
次にScalaを動かすためのプラグインを入れる。
まずは以下のサイトで、Update SitesのURLをコピーしてくる。
http://scala-ide.org/download/current.html
自分が見た時は下記URLになっていた。
http://download.scala-ide.org/releases-29/stable/site
Eclipseを立ち上げ、メニューバーから以下の操作を行う。
Help → Install New Software... → Add Nameに適当な文字(eclipse-scalaとか)を、Locationでさっき取ってきたUpdate SitesのURLを入力してOK Pending...と出るのでしばし待つ(この処理には数分かかる場合がうんたら) → Pendingが終わって選択可能なプラグインが表示される Scala IDE for Eclipseにチェックを入れて、Next Next → Accept → Finish → インストールが始まる → 再起動する聞かれるので再起動する またなんか聞かれるのでデフォルトのままOKして、再起動
これでMavenとScalaの用意はできた。
ScalaTestを実行できるプロジェクトを作成する。
まずは下記の手順でMavenプロジェクトを作成。
File → New → Other Maven → Maven Projectを選択 → Next Create a simple projectをチェック → Next Group IdとArtifact Idに適当な文字を入れる(今回は試しに、Group Idはjp.mwsoft.sample、Artifact Idはjava-scala-testとする) Finish
次にプロジェクトでScalaのコードが利用できるようにするために、以下の手順を実行。
出来上がったプロジェクトを右クリック → Configure → Add Scala Nature
これでScalaも利用できるMavenプロジェクトができた。
出来上がったプロジェクトは、テスト用フォルダがsrc/test/javaになっている。なんとなくこれはsrc/test/scalaに変更しておく。別に変えなくても動く。気分の問題。
pom.xmlを開いて、ScalaTestとかのDependencyを設定する。
Dependencyに入れるものは下記。
・ScalaTest ・Scala Compiler ・JUnit
記述内容はこんな感じ。
<!-- ScalaのVersionは可変にできるようにしとく --> <properties> <scala-version>2.9.2</scala-version> </properties> <!-- Dependencyの設定はこんな感じ --> <dependency> <groupId>org.scala-lang</groupId> <artifactId>scala-compiler</artifactId> <version>${scala-version}</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency> <dependency> <groupId>org.scalatest</groupId> <artifactId>scalatest_${scala-version}</artifactId> <version>1.8</version> <scope>test</scope> </dependency>
これでEclipse上でScalaTestを動かせるようになった。
準備ができたので、さっそくテストコードを実行してみる。
まずはsrc/main/java(テストじゃなく本体のコードを置くところ)に、下記のような足し算をするだけのやる気のないクラスを作る。
package jp.mwsoft.sample.scalatest;
public class SampleClass {
public static int add(int i, int j) {
return i + j;
}
}
次にsrc/main/scala(テストコードを置くところ)に上のコードと同じパッケージを作り、下記の手順でScalaのファイルを作る。
New → Scala Wizards → Scala Classを選択 → Next クラス名にSmapleClassTestと入力してFinish
出来上がったファイルに下記のような内容を記述する。
package jp.mwsoft.sample.scalatest
import org.scalatest.junit.JUnitRunner
import org.junit.runner.RunWith
import org.scalatest.matchers.ShouldMatchers
import org.scalatest.FlatSpec
@RunWith( classOf[JUnitRunner] )
class SampleClassTest extends FlatSpec with ShouldMatchers {
it should "1+2は3だよ" in {
SampleClass.add( 1, 2 ) should equal( 3 )
}
}
あとは下記のようにJUnitテストとしてこのコードを実行すると、見慣れたJUnit風の結果が返ってくる。
Run → Run As → JUnit Test
結果画面
これでEclipseを使って手軽にテストコードを試せる。
Specsでも多分同じ感じでいける。
Eclipse上では動いたので、次はMavenでも動きをチェックする。とりあえずプロジェクトのあるフォルダで下記のコマンドを実行。
$ mvn test ------------------------------------------------------- T E S T S ------------------------------------------------------- Running jp.mwsoft.sample.scalatest.SampleClassTest Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.092 sec Results : Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
テストコードのコンパイル設定とか何もしてないのに、ちゃんとテストされた。
これはEclipseがコンパイルを実行して、所定の場所にclassファイルを生成してくれているから。mvn cleanしてからmvn testを実行すると、「There are no tests to run」(テストは1個もないよ)と言われる。
このままだと途中でテストをすっ飛ばすミスが発生しそうなので、テスト実行時に自動でテストコードのコンパイルが走るように、pom.xmlに下記のように記述する。
<build> <sourceDirectory>src/main/java</sourceDirectory> <testSourceDirectory>src/test/scala</testSourceDirectory> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.7</source> <target>1.7</target> </configuration> </plugin> <plugin> <groupId>org.scala-tools</groupId> <artifactId>maven-scala-plugin</artifactId> <executions> <execution> <id>test-compile</id> <goals> <goal>testCompile</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
Javaのコンパイラが1.7に設定されているのは趣味の問題。ダイヤモンドとトライウィズなんとかさんなしでは生きられない身体になってきた。
これでmvn testとかmvn deployをした時に自動でテストコードもコンパイルされるようになった。
ただ、これをやるとEclipseのm2eプラグインがexecutionのところにエラーをマークしてしまう(動作的には問題はなかったはず)。
エラー印が目に入るのは精神的によろしくない。解決方法としては、以下が挙げられる。
これでJUnitの代わりにScalaTestが使えるようになった。めでたし。
今のままだとmvn testした際のテスト結果はtarget/surefire-reportsの下を見にいかないといけない。
maven-scalatest-pluginを使えば、こんな感じで何をテストしてどこが失敗したかをコンソール上に分かりやすく出力できる。
Run starting. Expected test count is: 3 DiscoverySuite: SampleClassTest: - 1+2は3だよ - 3+3は6だよ - 5+8は12だよ *** FAILED *** 13 did not equal 12 (SampleClassTest.scala:20) Run completed in 289 milliseconds. Total number of tests run: 3 Suites: completed 2, aborted 0 Tests: succeeded 2, failed 1, ignored 0, pending 0 *** 1 TEST FAILED ***
pom.xmlの記述はこんな感じ。そのまま使うとsurefire-pluginが効いてしまうので、そちらをskipしてscalatest-maven-pluginを利用している。
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.7</version> <configuration> <skipTests>true</skipTests> </configuration> </plugin> <plugin> <groupId>org.scalatest</groupId> <artifactId>scalatest-maven-plugin</artifactId> <version>1.0-M2</version> <executions> <execution> <goals> <goal>test</goal> </goals> </execution> </executions> </plugin>
最後にScalaTestの紹介も兼ねて少しだけテストコードを書いてみる。
TDD(テスト駆動)っぽい書き方と、BDD(振る舞い駆動)っぽい書き方が用意されている。JUnitを使っていた人にはTDDの方が馴染みがある気がするので、本例はそっちっぽい感じで。
例えば、(1, 5, 3)という3つの要素が入ったListを返すJavaのメソッドがあったとする。
public static List getList() {
return Arrays.asList(1, 5, 3);
}
下記のような感じで、FunSuiteをextendsして、assertを書いてテストする。比較は===(イコール3つ)を用いる。
class TDDSampleTest extends FunSuite {
val list = TDDSample.getList();
test( "getListは3つの要素を返すよ" ) {
assert( list.size === 3 )
}
test( "getListは1,5,3の要素を返すよ" ) {
assert( list === Arrays.asList( 1, 5, 3 ) )
}
}
Eclipse上で実行すると下記のようにテストした内容ごとにグリーンが出る。
テストがエラーになった場合は、下記のように原因も表示される。
ScalaTestとかSpecsは英文っぽくテストが書けるところが面白いような、そうでもないような。
import java.util.Arrays
import scala.collection.JavaConversions._
import org.junit.runner.RunWith
import org.scalatest.matchers.ShouldMatchers
import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner
@RunWith( classOf[JUnitRunner] )
class TDDSampleTest extends FunSuite with ShouldMatchers {
val list = Arrays.asList( 10, 5, 4, 4 )
test( "listの0番目の要素は10だよ" ) {
list( 0 ) should be( 10 )
}
test( "listの1番目の要素は、4〜8(6-2〜6+2)の間の数字だよ" ) {
list( 1 ) should be( 6 plusOrMinus 2 )
}
test( "listの長さは4だよ" ) {
list should have length ( 4 )
}
test( "listは要素の中に5か6がいるよ" ) {
list should ( contain( 5 ) or contain( 6 ) )
}
}
他にもいろんな表現ができる。詳くはScalaTestのUserGuid参照。
あとはBeanのチェックが楽なところがありたがいかも。例えば下記のようなありがちなJavaのクラスがいたとする。
public class TDDSample3 {
private String str;
private int i;
public String getStr() {
return str;
}
public void setStr(String str) {
this.str = str;
}
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
}
上記クラスは「str」と「i」の2つの値を持っている。このクラスは下記のようにテストができる。
val sample3 = new TDDSample3()
sample3.setStr( "文字列" )
sample3.setI( 30 );
test( "sample3は「文字列」と「30」を格納してるよ" ) {
sample3 should have(
'str( "文字列" ),
'i( 30 )
)
}
個人的には、この辺りの機能とScalaのコレクション操作辺りが使いたくてScalaTestにした感がある。
そういえば自分がテストコードをそこそこに書くようになったのは、Specsを知ってからだったっけか。
SpecsとかScalaTestくらいの記述の簡易さなら、結果を目視で確認しづらいところについてはコードを書いた方がテストしやすいや、なんてことを思った記憶がある。
記述が楽だと書く気力も湧きやすいよね。
テストコードだけScalaというパターンは、会社でScalaを導入したい時なんかにも有効な手だと言われている。
最初から全部Scalaでと言うとハードル高いけど、本例のような感じでテストコードのみであれば、うまいこと テストツールの一種ですとか言って騙せば 有用性を説明すれば、通ることもあるかもしれない。