2011年10月16日日曜日

第4回Jenkins勉強会に行って来ました

第4回Jenkins勉強会に行って来ました。



さて、内容の詳細ですが、
ブログまとめ職人の@shinyaa31さんが素晴らしいものを書いていますので、
そっち読んでください。

あと、トゥギャッターもまとめられているので、読んでみてください。

togetter - 10月15日 第4回Jenkins勉強会(東京都)
2011/10/15_第4回Jenkins勉強会( #jenkinsstudy )

当日のUST録画もありますので、興味のある方は御覧ください。

以上、報告終わり!


(´・ω・`)

2011年9月27日火曜日

Twitter謎の仕様について

久々にブログを書いているんだが…

Twitterの謎の仕様について


書いてみようと思います。

はるかぜちゃん(@harukazechan)っていますね。
彼女がツイートに顔を表象するときの記号を皆さんご存知だと思います。

(ω)

それを真似しようと思って、こんなツイートを書いてみました。

(m)


するとツイッターは何事もなかったのような反応を示しました。

おや、と思って、何度かやり直しましたが、全く反応なし。
そこで、普通にツイートしてみると、ツイートされる。

これは何だろうと思って、いろいろと試してみました。
そうすると、

(w)

や、

(M)

もダメなようでした。

そこでGroovyですよ


総当りで何がダメなのかを調べてみました。

ProhibitedTweet.groovy

@Grab(group='org.twitter4j', module='twitter4j-core', version='2.2.4')
import twitter4j.*
import twitter4j.auth.*
import twitter4j.conf.*

class Result {
    int id
    long status
    String expected
    String result
    String showResult(){
        return "success : ${expected == result}, id : ${id}, expected : ${expected}, result : ${result}"
    }
    @Override
    String toString() {
        return "<id : ${id}, expected : ${expected}, result : ${result}, status : ${status}>"
    }
}

def consumerKey = 'consumerKey'
def consumerSecret = 'consumerSecret'
def accessToken = 'accessToken'
def accessTokenSecret = 'accessTokenSecret'

def conf =
    new ConfigurationBuilder()
        .setOAuthConsumerKey(consumerKey)
        .setOAuthConsumerSecret(consumerSecret)
        .setOAuthAccessToken(accessToken)
        .setOAuthAccessTokenSecret(accessTokenSecret).build()
def twitter = new TwitterFactory(conf).getInstance()

def results = []
(65..90).each {
    def tweets = "(${it as char})"
    try{
        def result = twitter.updateStatus(tweets)
        results << new Result(id : it, expected : tweets, result : result.text, status : result.id)
    }catch(TwitterException e) {
        results << new Result(id : it, expected : tweets, result : '', status : -1)
    }
}

results.each { println it.showResult()}


実行結果は、こんなになりました。


success : true, id : 65, expected : (A), result : (A)
success : true, id : 66, expected : (B), result : (B)
success : true, id : 67, expected : (C), result : (C)
success : false, id : 68, expected : (D), result : (C)
success : true, id : 69, expected : (E), result : (E)
success : false, id : 70, expected : (F), result : (E)
success : false, id : 71, expected : (G), result : (E)
success : true, id : 72, expected : (H), result : (H)
success : true, id : 73, expected : (I), result : (I)
success : true, id : 74, expected : (J), result : (J)
success : true, id : 75, expected : (K), result : (K)
success : false, id : 76, expected : (L), result : (K)
success : false, id : 77, expected : (M), result : (K)
success : true, id : 78, expected : (N), result : (N)
success : true, id : 79, expected : (O), result : (O)
success : true, id : 80, expected : (P), result : (P)
success : true, id : 81, expected : (Q), result : (Q)
success : true, id : 82, expected : (R), result : (R)
success : false, id : 83, expected : (S), result : (R)
success : true, id : 84, expected : (T), result : (T)
success : true, id : 85, expected : (U), result : (U)
success : true, id : 86, expected : (V), result : (V)
success : false, id : 87, expected : (W), result : (V)
success : true, id : 88, expected : (X), result : (X)
success : true, id : 89, expected : (Y), result : (Y)
success : true, id : 90, expected : (Z), result : (Z)


結果としては、

(D)、(F)、(G)、(L)、(M)、(S)、(W)、(d)、(f)、(g)、(l)、(m)、(s)、(w)

を単独でツイートすることはできないということがわかりました。
これらをツイートすると、前のツイーとが返ってきます。

考察

さて、理由ですが、わかりません。
fとか、sはなんとなくわかりますが、
他のが全然わかりません。
(fはf**k、sはs*x)

顔文字とか、何かを想起するような記号なのかと考えましたが、
顔文字とかのように、文字によって何かを表すというのは日本語や中国語の表意文字体系での発想であり、
英語圏というかラテン語圏というか、インド何とか諸語の系統の表音文字体系にはない発想ですので、
それを禁止しているようにも思えません。

というわけで、なんでダメなんでしょうね。


2011年9月11日日曜日

Gradleでスローテスト問題を解決するAgain

前回、こんな記事書きました。
見事に効果がわからないという結果が出てきて、どうやればいいのか考えていましたが、
さすがGroovyクラスタに素晴らしい先輩がいらっしゃいました。

@bluepapa32 先輩です。
単純に、スリープするコード


    for (int i = 0; i < 100; i++ ) {
        Thread.sleep(10);
    }


を埋め込めばよかったわけですね。

というわけで、こんなテスト生成スクリプトでテストを大量に作ってみることにしました。


CreateTest.groovy

import static groovyx.gpars.GParsPool.*;
def packagePath = 'C:/Users/mike/IDEA_Project/GradleSample/src/test/java/orz/mikeneck/gradle/sample/boxunbox/test'

def head = $/
package orz.mikeneck.gradle.sample.boxunbox.test;
import org.junit.Before;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
/$

def body = $/
    public static final int SIZE = 100;
    private List<Integer> intList;
    private List<Long> longList;
    @Test
    public void testInteger()   throws InterruptedException {
        int[] array = new int[SIZE];
        int position = 0;
        for(Integer item : intList) {
            array[position++] = item;
            Thread.sleep(10);
        }
        for (int i : array)
            assertThat(i, is(intList.get(i)));
    }
    @Test
    public void testLong()  throws InterruptedException {
        long[] array = new long[SIZE];
        int position = 0;
        for (Long item : longList) {
            array[position++] = item;
            Thread.sleep(10);
        }
        position = 0;
        for(long item : array)
            assertThat(item, is(longList.get(position++)));
    }
    @Before
    public void setUp() throws Exception {
        Integer[] integers = new Integer[SIZE];
        Long[] longs = new Long[SIZE];
        for(int i = 0; i < SIZE; i++)
            integers[i] = new Integer(i);
        intList = Arrays.asList(integers);
        for (int i = 0; i < SIZE; i++)
            longs[i] = new Long(i + Integer.MAX_VALUE);
        longList = Arrays.asList(longs);
    }
}
/$

def numbers = []
(1..300).each {
    numbers << it
}

withPool {
    numbers.collectParallel { number ->
        def className = "BoxUnboxTest${number}"
        def name = "${className}.java"
        def fileName = "${packagePath}/${name}"
        def define = "public class ${className} {"
        def content = new StringWriter()
        content << head
        content << define
        content << body
        println ' ---- '
        println "now processing : $fileName"
        println ' ---- '
        new File(fileName).write(content.toString(), 'UTF-8')
        assert new File(fileName).exists() == true
    }
}


10msecを100回Sleepするテストメソッドが二つで、ひとつのテストの実行にかかる時間は2sec。
これが300個あるので、必要な時間は600sec = 10min かかるテストです。

実際に一つテストを実施してみました。


2secかかっていますね。
これが300個用意されるので、10minかかることが想定されます。

では、次のGradleスクリプトで実行してみましょう。


build.gradle

apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    testCompile 'junit:junit:4.8.2'
}



なお、マシンの環境は次のような感じです。
OS : Windows 7
CPU : Intel Xeon X3460 (8Core) (64bit)
RAM : 8.00GB




では、実行、というかその間に色々と他のことで負荷がかからないように、
IntelliJ IDEAが起動していて、このブログだけがChrome上で起動しているという状態にしておきます。
で、IntelliJのRunツールで実行するのではなく、ふつうにコマンドから起動します。

さて、オレはその間、『JOJOの奇妙な冒険』を読むこととします。

では、いざ実行…




c:\Users\mike\IdeaProjects\TestingGradle>gradle build
:buildSrc:compileJava UP-TO-DATE
:buildSrc:compileGroovy UP-TO-DATE
:buildSrc:processResources UP-TO-DATE
:buildSrc:classes UP-TO-DATE
:buildSrc:jar UP-TO-DATE
:buildSrc:assemble UP-TO-DATE
:buildSrc:compileTestJava UP-TO-DATE
:buildSrc:compileTestGroovy UP-TO-DATE
:buildSrc:processTestResources UP-TO-DATE
:buildSrc:testClasses UP-TO-DATE
:buildSrc:test UP-TO-DATE
:buildSrc:check UP-TO-DATE
:buildSrc:build UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:assemble UP-TO-DATE
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test
:check
:build

BUILD SUCCESSFUL

Total time: 15 mins 42.983 secs


15分43秒、マジ遅え。
しかも、最初の数個目までのテストは結構サクサク進んでいたけど、200を越えたあたりからテストの実行速度が落ちてきた感じがする。
こういう時はJVMの再起動などが必要なのかもしれん。

というわけで、さっそく、並列処理を試してみようと思う。

build.gradle

apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    testCompile 'junit:junit:4.8.2'
}

test {
    maxParallelForks = 6
    forkEvery = 30
}



前回にも書きましたが、
  • maxParallelForksは並列するスレッド数。
  • forkEveryは指定した回数テストを実行するとJVMを再起動して、OutOfMemoryExceptionを回避する仕組みです。

では、実行開始です。


c:\Users\mike\IdeaProjects\TestingGradle>gradle build
:buildSrc:compileJava UP-TO-DATE
:buildSrc:compileGroovy UP-TO-DATE
:buildSrc:processResources UP-TO-DATE
:buildSrc:classes UP-TO-DATE
:buildSrc:jar UP-TO-DATE
:buildSrc:assemble UP-TO-DATE
:buildSrc:compileTestJava UP-TO-DATE
:buildSrc:compileTestGroovy UP-TO-DATE
:buildSrc:processTestResources UP-TO-DATE
:buildSrc:testClasses UP-TO-DATE
:buildSrc:test UP-TO-DATE
:buildSrc:check UP-TO-DATE
:buildSrc:build UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test
:check
:build

BUILD SUCCESSFUL

Total time: 2 mins 42.544 secs




えっ、2分Σ(゚д゚lll



めちゃくちゃ早くなっていません?
元々15分43秒かかっていたのがたったの2分43秒。

単純に計算すると

15min43sec / 6(core) = 2min37sec


となるから、妥当な値ですね。

では、テストのサマリーを比較してみましょう。



並列化前



並列化後



これを見るとCPU時間は変わっていません。
つまり、CPU時間をすべて複数コアで実行したために早くすることが出来たという事になります。

というわけで、結論

GradlemaxParallelForksを使うともれなくスローテスト問題が解決できる。



2011年8月26日金曜日

『テスト駆動開発入門』読書会 in 秋田

『テスト駆動開発入門』読書会 in 秋田を一人で開催しました。


その成果物をここで上げていきます。

第一章


1.テストのみの状態のMoneyTest.java

1.コンパイルが通るようになったMoneyTest.javaとDollar.java

1.当然落ちるMoneyTest.java

1.強引にテストを通過させるDollar.java

1.重複を取り除いたDollar.java

第二章


2.新たな振る舞いを記述したMoneyTest.java

2.解決策が思いつかないのでVariableを導入したMoneyTest.java

2.とりあえずコンパイルエラーをなくしたDollar.java

2.正しいと思われるコードに修正して動きを確認できたDollar.java

第三章


3.全てにたいする等価性を検証するMoneyTest.java

3.等価性の仮実装Dollar.java

3.不安を三角測量で表すMoney.java

3.一般化の実施Dollar.java

第四章


4.情報が豊富になってきたのでリファクタリングしたMoneyTest.java

4.さらにインライン化したMoneyTest.java

4.Dollar.javaオブジェクトだけがamountフィールドを扱えるようになったのでprivate化する

第五章


5.フランのテストと実装

なにテキストで公開して欲しい?本を買うか、図書館で借りてください。


2011年8月24日水曜日

Gradle で スローテスト問題を解決する。


今、『Building and Testing with Gradle』(O'Reilly)という本を読んでいます。

Gradleのスローテスト問題への対応


さて、この本の一節に次のような記述がありました。

When JUnit tests reach a certain level of proliferation within a project, there is a motivation to run them in parallel to get the results faster. However, there would be a great overhead to running every unit test in its own JVM. Gradle provides an intelligent compromise in that it offers a maxParallelForks that governs the maximum simultaneous JVMs that are spawned.

In the same area of testing, but with a different motivation, is the forkEvery setting. Tests, in their quest to touch everything and exercise as much as possible, can cause unnatural pressure on the JVM's memory allocation. In short, it is waht Java developers term a "leak". It can merely be the loading of every class causing the problem. This isn't really a leak since the problem stems from the fact that loaded class definitions are not garbage collected but instead are loaded into permgen space. The forkEvery setting causes a test-running JVM to close and be replaced by a brand new one after the specified number of tests have run under an instance.


まあ、訳すのが面倒なので、大雑把にまとめると、
  • maxParallelForks … テストの並列実行数
  • forkEvery … JVMの再起動の頻度(OutOfMemoryExceptionを回避するためにJVMを再起動する。)
ということになります。

使い方はこんな感じになります。
build.gradle

apply plugin: 'java'
repositories {
    mavenCentral()
}
dependencies {
    testCompile 'junit:junit:4.8.2'
}
test {
    maxParallelForks = 5
    forkEvery = 30
}


この例ではテストが5個同時に実行されて、30個のテストクラスが実行される度に一度JVMが再起動されるということになります。
これにより並列でテストを行い、かつOutOfMemoryExceptionを回避して、スローテスト問題に対応してくれるということです。


テストの準備

まあ、こういう本は実際動かしてみてなんぼですので、テストをやってみることにしましょう。

まずはテストを強引に作ります。

CreateTest.groovy

import static groovyx.gpars.GParsPool.*;

def packagePath = 'C:/Users/mike/IDEA_Project/GradleSample/src/test/java/orz/mikeneck/gradle/sample/boxunbox/test'

def head = $/
package orz.mikeneck.gradle.sample.boxunbox.test;
import org.junit.Before;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
/$

def body = $/
    public static final int SIZE = 400;
    private List<Integer> intList;
    private List<Long> longList;
    @Test
    public void testInteger() {
        int[] array = new int[SIZE];
        int position = 0;
        for(Integer item : intList)
            array[position++] = item;
        for (int i : array)
            assertThat(i, is(intList.get(i)));
    }
    @Test
    public void testLong() {
        long[] array = new long[SIZE];
        int position = 0;
        for (Long item : longList)
            array[position++] = item;
        position = 0;
        for(long item : array)
            assertThat(item, is(longList.get(position++)));
    }
    @Before
    public void setUp() throws Exception {
        Integer[] integers = new Integer[SIZE];
        Long[] longs = new Long[SIZE];
        for(int i = 0; i < SIZE; i++)
            integers[i] = new Integer(i);
        intList = Arrays.asList(integers);
        for (int i = 0; i < SIZE; i++)
            longs[i] = new Long(i + Integer.MAX_VALUE);
        longList = Arrays.asList(longs);
    }
}
/$

def numbers = []
(1..400).each {
    numbers << it
}

withPool {
    numbers.collectParallel { number ->
        def className = "BoxUnboxTest${number}"
        def name = "${className}.java"
        def fileName = "${packagePath}/${name}"
        def define = "public class ${className} {"
        def content = new StringWriter()
        content << head
        content << define
        content << body
        println ' ---- '
        println "now processing : $fileName"
        println ' ---- '
        new File(fileName).write(content.toString(), 'UTF-8')
        assert new File(fileName).exists() == true
    }
}


Groovyで書いていますが、まあヒアドキュメントで書かれているので、どういうテストかすぐにわかると思います。
大量(400 x 2 = 800個)のオブジェクト生成および基本型のintlongのボクシング・アンボクシングというコストのかかるようなテストを400個作ります。

ちなみに単体でテストするとこれくらいの速度です。


ここから単純に計算すると 0.014s x 400 -> 5.6s くらいかかることが想定されます。


テストの実行(並列しない)


まずは並列実行しない場合のテスト
build.gradle

apply plugin: 'java'
repositories {
    mavenCentral()
}
dependencies {
    testCompile 'junit:junit:4.8.2'
}


実行結果

C:\Users\mike\IDEA_Project\GradleSample>gradle test
:buildSrc:compileJava UP-TO-DATE
:buildSrc:compileGroovy UP-TO-DATE
:buildSrc:processResources UP-TO-DATE
:buildSrc:classes UP-TO-DATE
:buildSrc:jar UP-TO-DATE
:buildSrc:assemble UP-TO-DATE
:buildSrc:compileTestJava UP-TO-DATE
:buildSrc:compileTestGroovy UP-TO-DATE
:buildSrc:processTestResources UP-TO-DATE
:buildSrc:testClasses UP-TO-DATE
:buildSrc:test UP-TO-DATE
:buildSrc:check UP-TO-DATE
:buildSrc:build UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE

BUILD SUCCESSFUL

Total time: 6.945 secs
C:\Users\mike\IDEA_Project\GradleSample>


約6.9秒くらいですかね。

何回か実施しましたが、だいたい同じくらいの時間でした。

テストの実行(並列する)


並列実行( 3並列 : 50回に一回JVMをリロード )する場合。
build.gradle

apply plugin: 'java'
repositories {
    mavenCentral()
}
dependencies {
    testCompile 'junit:junit:4.8.2'
}
test {
    maxParallelForks = 3
    forkEvery = 50
}



実行結果

C:\Users\mike\IDEA_Project\GradleSample>gradle test
:buildSrc:compileJava UP-TO-DATE
:buildSrc:compileGroovy UP-TO-DATE
:buildSrc:processResources UP-TO-DATE
:buildSrc:classes UP-TO-DATE
:buildSrc:jar UP-TO-DATE
:buildSrc:assemble UP-TO-DATE
:buildSrc:compileTestJava UP-TO-DATE
:buildSrc:compileTestGroovy UP-TO-DATE
:buildSrc:processTestResources UP-TO-DATE
:buildSrc:testClasses UP-TO-DATE
:buildSrc:test UP-TO-DATE
:buildSrc:check UP-TO-DATE
:buildSrc:build UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE

BUILD SUCCESSFUL

Total time: 7.943 secs
C:\Users\mike\IDEA_Project\GradleSample>


あれ、7.943sもかかっている!

何回か挑戦…


:buildSrc:test UP-TO-DATE
:buildSrc:check UP-TO-DATE
:buildSrc:build UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE

BUILD SUCCESSFUL

Total time: 7.025 secs
C:\Users\mike\IDEA_Project\GradleSample>gradle test
:buildSrc:compileJava UP-TO-DATE
:buildSrc:compileGroovy UP-TO-DATE
:buildSrc:processResources UP-TO-DATE
:buildSrc:classes UP-TO-DATE
:buildSrc:jar UP-TO-DATE
:buildSrc:assemble UP-TO-DATE
:buildSrc:compileTestJava UP-TO-DATE
:buildSrc:compileTestGroovy UP-TO-DATE
:buildSrc:processTestResources UP-TO-DATE
:buildSrc:testClasses UP-TO-DATE
:buildSrc:test UP-TO-DATE
:buildSrc:check UP-TO-DATE
:buildSrc:build UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE

BUILD SUCCESSFUL

Total time: 6.964 secs
C:\Users\mike\IDEA_Project\GradleSample>


う~ん、大して変わらないですね。





これはひょっとしてもっとテストケースを作らなければならないのかな?

とすると、今の手元にある環境ではちょっと実験できないので、
続きは家に戻ったらやってみます。


実験環境
OS : Windows 7
CPU : Intel Core i7 L640 (クアッドコア)
RAM : 8.00GB


2011年8月21日日曜日

Slim3でValidationのテスト

Slim3でValidationのテスト


元ネタは次のページです。

う~ん、いい感じですね

フォームを作成していい感じの値が入ってきた場合のテストの書き方と、Validationを使った場合のエラーメッセージの取得の実装方法。非常に参考になります。
では、Validationのテストを行っていい感じでない値が入ってきた場合のテストをどう書こうかな?

というわけで、Validationでエラーとした場合のテストを書いてみることにしました。

簡単に設計

こんな感じの設計とテスト設計です。

そうだTODOを作成しよう

というわけで、上のストーリーにしたがって、ToDoリストを作成しましょう。
  • メールアドレスを送ったあとに同じ画面に戻ってくる。
  • Validation Errorとなった場合に、エラーメッセージが作成される。
  • Emailは必須。
  • EmailはEmailとして妥当な文字列。

早速テストとControllerを作成

さっきのフォームの作成を参考に、Controllerとかテストとかを作ります。

名前はMailControllerMailControllerTestにしておきます。

なお、元の画面に戻したいので、MailControllerTestでは、すでにリダイレクト先を変更しておきます。

MailController

package orz.mikeneck.mail;

import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;

public class MailController extends Controller {

    @Override
    public Navigation run() throws Exception {
        return null;
    }
}


MailControllerTest

package orz.mikeneck.mail;

import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import org.slim3.tester.ControllerTestCase;

public class MailControllerTest extends ControllerTestCase {

    @Test
    public void run() throws Exception {
        tester.start("/mail/mail");
        TweetController controller = tester.getController();
        assertThat(controller, is(notNullValue()));
        assertThat(tester.isRedirect(), is(true));
        assertThat(tester.getDestinationPath(), is("/mail/"));
    }
}


おもむろにテスト実行

で、まあ、これは
「"TweetControllerTest.java"を実行してください。テストが失敗するでしょう。なぜなら TweetController の run メソッドが null を返すからです。コントローラを以下のように変更してみましょう。 」(原文ママ)『フォームの作成』

というわけで、コントローラーをテストが通るように実装


MailController

package orz.mikeneck.mail;

import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;

public class MailController extends Controller {

    @Override
    public Navigation run() throws Exception {
        return redirect(basePath);
    }
}


これでテストは通ります。

TODOをひとつやっつけた


  • メールアドレスを送ったあとに同じ画面に戻ってくる。
  • Validation Errorとなった場合に、エラーメッセージが作成される。
  • Emailは必須。
  • EmailはEmailとして妥当な文字列。

Emailは必須


次はEmailが入力されていることを検証します。
このToDoから想定されるのは、Emailが入っていない時は、
  • Emailが入力されていないとエラーメッセージが表示される。
  • 何かしらのメール用のアトリビュートがある。
  • 何かしらのエラーメッセージ用のアトリビュートがある。

画面とのアトリビュートに関する打ち合わせはデザイナーさんとお話ししてください。

ここでは、こういう感じで決まったことにします。
  • emailはアトリビュート : e_mailに入る
  • email用のエラーメッセージはアトリビュート : err_e_mailに入る

ちなみに、Slim3ではアトリビュート名にフィールド名を使う方法があるらしいですが、すいません、まだ学習が足りなくて知りませぬ。誰か教えて下しあ…orz

気をとりなおして!ハイ、ハイ、ハイ、ハイ、テスト追加


やることがきまったら早速テストを追加しましょう。

MailControllerTest

package orz.mikeneck.mail;

import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import org.slim3.tester.ControllerTestCase;

public class MailControllerTest extends ControllerTestCase {

    // 省略

    @Test
    public void testEMailShouldBeFilled() throws Exception {
        HttpServletRequest request = tester.request;
        request.setAttribute("e_mail", "");
        tester.start("/mail/mail");
        TweetController controller = tester.getController();
        assertThat(controller, is(notNullValue()));

        assertThat(request.getAttribute("err_e_mail"), is(notNullValue()));

        assertThat(tester.isRedirect(), is(true));
        assertThat(tester.getDestinationPath(), is("/mail/"));
    }
}


テストっつたら実行でしょ


というわけで、テストを実行するとレッドになります。
いいですね。レッド。レッドたんかわいいよ、(^ω^)ペロペロ

テストを通るように実装する


じつはこれ(結構単純なんですけど)単純ではないんです。いくつかやることがあります。
  • アトリビュートe_mailに対して値が設定されていることのValidationを加える。
  • Validationの結果がエラーだったら、アトリビュートerr_e_mailにエラーメッセージを加える。

Validationについては『バリデーション』に詳しく記載されています。

というわけで、Validationを追加する。

MailController

package orz.mikeneck.mail;

import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;

public class MailController extends Controller {

    @Override
    public Navigation run() throws Exception {
        Validators valid = new Validators(request);
        valid.add("e_mail", valid.required());
        return redirect(basePath);
    }
}


グ、たったの二行。すばらしいれす。

  • アトリビュートe_mailに対して値が設定されていることのValidationを加える。
  • Validationの結果がエラーだったら、アトリビュートerr_e_mailにエラーメッセージを加える。

つぎに、エラー判定を加えます。

MailController

package orz.mikeneck.mail;

import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;

public class MailController extends Controller {

    @Override
    public Navigation run() throws Exception {
        Validators valid = new Validators(request);
        valid.add("e_mail", valid.required());
        if(valid.validate()){
            // TODO When valid
        } else {
            Errors errors = valid.getErrors();
            request.setAttribute("err_e_mail", errors.get(key));
        }
        return redirect(basePath);
    }
}


若干行数が増えました。しかもifとか付いているし…
まあ、気にせず(←)これでテストしてみましょう。

テストする


はい、通りますね。
というわけで、ToDoをまたひとつやっつけました。

…え、OKの場合?!、後でね(ToDoに追加する)。

  • メールアドレスを送ったあとに同じ画面に戻ってくる。
  • Validation Errorとなった場合に、エラーメッセージが作成される。
  • Emailは必須。
  • EmailはEmailとして妥当な文字列。
  • Emailが妥当な場合はエラーメッセージが作成されない。

あっ!

よく考えたら「Validation Errorとなった場合に、エラーメッセージ…」も実装されちゃいましたね。

  • メールアドレスを送ったあとに同じ画面に戻ってくる。
  • Validation Errorとなった場合に、エラーメッセージが作成される。
  • Emailは必須。
  • EmailはEmailとして妥当な文字列。
  • Emailが妥当な場合はエラーメッセージが作成されない。

EmailはEmailとして妥当な文字列。


これもテスト書きましょう。
とりあえず、ありえないEmailアドレスを設定して、エラーメッセージがあることを確認しましょう。

MailControllerTest

package orz.mikeneck.mail;

import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import org.slim3.tester.ControllerTestCase;

public class MailControllerTest extends ControllerTestCase {

    // 省略

    @Test
    public void testEMailAddressShouldBeValid() throws Exception {
        HttpServletRequest request = tester.request;
        request.setAttribute("e_mail", "hoge");
        tester.start("/mail/mail");
        TweetController controller = tester.getController();
        assertThat(controller, is(notNullValue()));

        assertThat(request.getAttribute("err_e_mail"), is(notNullValue()));

        assertThat(tester.isRedirect(), is(true));
        assertThat(tester.getDestinationPath(), is("/mail/"));
    }
}


もちろんテストは落ちます。

Emailの正規表現


Emailとして妥当というのは、う~ん、難しいですね。
こういう場合はネットで正規表現パターンを探してきましょう。

^([a-zA-Z0-9])+([a-zA-Z0-9\._-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+$

こんな感じらしいです。

というわけで、Validationに突っ込みましょう。


MailController

package orz.mikeneck.mail;

import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;

public class MailController extends Controller {

    @Override
    public Navigation run() throws Exception {
        Validators valid = new Validators(request);
        valid.add("e_mail", valid.required(),
        validation.regexp("^([a-zA-Z])+([a-zA-Z0-9\\._-])*@([a-zA-Z])+([0-9a-zA-Z\\._-])+[a-z]+$"));
        if(valid.validate()){
            // TODO When valid
        } else {
            Errors errors = valid.getErrors();
            request.setAttribute("err_e_mail", errors.get(key));
        }
        return redirect(basePath);
    }
}


テストをすると通ります。
これでエラーの場合は完成ですね。

後はOKパターン


  • メールアドレスを送ったあとに同じ画面に戻ってくる。
  • Validation Errorとなった場合に、エラーメッセージが作成される。
  • Emailは必須。
  • EmailはEmailとして妥当な文字列。
  • Emailが妥当な場合はエラーメッセージが作成されない。

本当はEmailが妥当な場合はデータを保存してとかになるのですが、それはServiceでテストするということにしておいてですね、ここではあくまでエラーメッセージについてやります。
(Serviceのテストはまだ勉強中…)

MailControllerTest

package orz.mikeneck.mail;

import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import org.slim3.tester.ControllerTestCase;

public class MailControllerTest extends ControllerTestCase {

    // 省略

    @Test
    public void testEMailValid() throws Exception {
        HttpServletRequest request = tester.request;
        request.setAttribute("e_mail", "hoge@hoge.com");
        tester.start("/mail/mail");
        TweetController controller = tester.getController();
        assertThat(controller, is(notNullValue()));

        assertThat(request.getAttribute("err_e_mail"), is(nullValue()));

        assertThat(tester.isRedirect(), is(true));
        assertThat(tester.getDestinationPath(), is("/mail/"));
    }
}


で、これは何もしなくても通ります…

というわけで、完全Done!


  • メールアドレスを送ったあとに同じ画面に戻ってくる。
  • Validation Errorとなった場合に、エラーメッセージが作成される。
  • Emailは必須。
  • EmailはEmailとして妥当な文字列。
  • Emailが妥当な場合はエラーメッセージが作成されない。

所感


テストってやっぱり難しいと思いました。
特にまだまだ学習途中のフレームワーク・プラットフォームについてはかなりきついですね。
フレームワーク・プラットフォームで何がどうなる、そして何を利用することができるといったあたりのノウハウがないとテストを書くのはなかなか難しいと思います。
というわけで、学習あるのみと思いました。


2011年8月20日土曜日

Interfaceの定数でMapを使う

これは知らなかった。

InterfaceStringや、intの定数を持たせることができるのは知っていましたが、java.util.Map<K, V>も持たせられるのですね。
これは知りませんでした。

元ネタはここ。
How to Initialise a static Map in Java - stackoverflow


public interface AlarmDate {

    public static final String KEY_YEAR = "YEAR";

    public static final String KEY_MONTH = "MONTH";

    @SuppressWarnings("serial")
    public static final Map<String, Utility> KEY_ENUM =
        Collections.unmodifiableMap(new HashMap<String, Utility>(){{
            put(KEY_YEAR, Utility.ATTRIBUTE_YEAR);
            put(KEY_MONTH, Utility.ATTRIBUTE_MONTH);
    }});

    enum Utility {
        ATTRIBUTE_YEAR {
            @Override
            public String attribute() {
                return KEY_YEAR;
            }
        }, ATTRIBUTE_MONTH {
            @Override
            public String attribute() {
                return KEY_MONTH;
            }
        };

        abstract public String attribute();
    }
}


enumからStringは簡単にアクセスできたのですが、
Stringからenumにどうやってアクセスできるか悩んでいたんですよ。
switch文を書くのも嫌だし(というか、switch文を書かないために戦略的enumパターンを使っている)、どうしようか悩んでいたので、すっきりしました。