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パターンを使っている)、どうしようか悩んでいたので、すっきりしました。


2011年8月19日金曜日

Interfaceに内部enumをつけて、そのenumが他のinterfaceを実装していて、自己満足なコードができている。

Interfaceに内部enumをつけて、そのenumが他のinterfaceを実装していて、自己満足なコードができている。


と、ツイートしたので、その自己満足なコードを晒してみる。


package orz.mikeneck.gae.slim3.model.alarm;

import java.util.Date;
import java.util.TimeZone;

import org.slim3.controller.validator.Validator;
import org.slim3.controller.validator.Validators;

import orz.mikeneck.gae.slim3.util.AttributeKeyValue;
import orz.mikeneck.gae.slim3.util.AttributeValidation;

public interface AlarmDate {

    public static final TimeZone TIME_ZONE = TimeZone.getTimeZone("Asia/Tokyo");

    public static final String KEY_YEAR = "YEAR";

    public static final String KEY_MONTH = "MONTH";

    public static final String KEY_DATE = "DATE";

    public static final String KEY_HOUR = "HOUR_OF_DAY";

    public static final String KEY_MINUTE = "MINUTE";

    public static final long MONTH_MAX = 12;

    public static final long MONTH_MIN = 1;

    public static final long DATE_MAX = 31;

    public static final long DATE_MIN = 1;

    public static final long HOUR_MAX = 23;

    public static final long HOUR_MIN = 0;

    public static final long MINUTE_MIN = 0;

    public static final long MINUTE_MAX = 55;

    public int year();

    public int month();

    public int date();

    public int hour();

    public int minute();

    public Date getCreatedAt();

    enum AttributeMapper implements AttributeKeyValue<AlarmDate>, AttributeValidation {
        ATTRIBUTE_YEAR {
            @Override
            public String key() {
                return KEY_YEAR;
            }

            @Override
            public <T> T value(AlarmDate model, Class<T> klass) {
                return klass.cast(model.year());
            }

            @Override
            public Class<?> type() {
                return Integer.class;
            }

            @Override
            public Validator[] validation(Validators valid) {
                return new Validator[] {
                    valid.required(),
                    valid.integerType()};
            }
        }, ATTRIBUTE_MONTH {
            @Override
            public String key() {
                return KEY_MONTH;
            }

            @Override
            public <T> T value(AlarmDate model, Class<T> klass) {
                return klass.cast(model.month());
            }

            @Override
            public Class<?> type() {
                return Integer.class;
            }

            @Override
            public Validator[] validation(Validators valid) {
                return new Validator[] {
                    valid.required(),
                    valid.integerType(),
                    valid.longRange(MONTH_MIN, MONTH_MAX)};
            }
        }, ATTRIBUTE_DATE {
            @Override
            public String key() {
                return KEY_DATE;
            }

            @Override
            public <T> T value(AlarmDate model, Class<T> klass) {
                return klass.cast(model.date());
            }

            @Override
            public Class<?> type() {
                return Integer.class;
            }

            @Override
            public Validator[] validation(Validators valid) {
                return new Validator[] {
                    valid.required(),
                    valid.integerType(),
                    valid.longRange(DATE_MIN, DATE_MAX)};
            }
        }, ATTRIBUTE_HOUR_OF_DAY {
            @Override
            public String key() {
                return KEY_HOUR;
            }

            @Override
            public <T> T value(AlarmDate model, Class<T> klass) {
                return klass.cast(model.hour());
            }

            @Override
            public Class<?> type() {
                return Integer.class;
            }

            @Override
            public Validator[] validation(Validators valid) {
                return new Validator[] {
                    valid.required(),
                    valid.integerType(),
                    valid.longRange(HOUR_MIN, HOUR_MAX)
                };
            }
        }, ATTRIBUTE_MINUTE {
            @Override
            public String key() {
                return KEY_MINUTE;
            }

            @Override
            public <T> T value(AlarmDate model, Class<T> klass) {
                return klass.cast(model.minute());
            }

            @Override
            public Class<?> type() {
                return Integer.class;
            }

            @Override
            public Validator[] validation(Validators valid) {
                return new Validator[] {
                    valid.required(),
                    valid.integerType(),
                    valid.longRange(MINUTE_MIN, MINUTE_MAX)
                };
            }
        };

        @Override
        abstract public String key();

        @Override
        abstract public <T> T value(AlarmDate model, Class<T> klass);

        @Override
        abstract public Class<?> type();

        @Override
        abstract public Validator[] validation(Validators valid);

        @Override
        public void validate(Validators valid) {
            valid.add(key(), validation(valid));
        }
    }
}


こやつは、Slim3のModelクラスのインターフェースなのですが、何がいいかというと、requestのマッピングとか、Validationのコードなどが劇的にキレイになる。

Mapping

package orz.mikeneck.gae.slim3.controller.alarm;

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

import orz.mikeneck.gae.slim3.model.alarm.AlarmDate;
import orz.mikeneck.gae.slim3.model.alarm.AlarmDateModel;

public class IndexController extends Controller {

    @Override
    public Navigation run() throws Exception {
        AlarmDate model = new AlarmDateModel();
        for(AlarmDate.AttributeMapper mapper : AlarmDate.AttributeMapper.values())
            request.setAttribute(mapper.key(), mapper.value(model, mapper.type()));
        return forward("index.jsp");
    }
}


Validators

package orz.mikeneck.gae.slim3.controller.alarm;

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

import orz.mikeneck.gae.slim3.model.alarm.AlarmDate;

public class WakeController extends Controller {

    @Override
    public Navigation run() throws Exception {
        Validators valid = new Validators(request);
        for(AlarmDate.AttributeMapper mapper : AlarmDate.AttributeMapper.values())
            mapper.validate(valid);
        return redirect(basePath);
    }
}


Validationとかの部分が二行で書けるので、コントローラーにモデルに関する複雑なことを書かんでよくなる。
まあ、もっといいやりかたがあるんだろうけど、コレが今のオレの程度…orz


2011年8月15日月曜日

OAuth2.0について調べてまとめてみた。

OAuth2.0について調べてまとめてみた。

Google Data Apiを触ってみようと思ったけど、3-legged OAuthよりもOAuth2.0が推奨されていて、それが何のことかよくわからなかったので調べてみることにした。
(Google Data ApiのJavaライブラリーは3-legged OAuthを使っているみたいだけど…)

まあ、詳しくは調べていないので、雑です。

OAuthのフロー(1)
UserはClientを起動する。

OAuthのフロー(2)
ClientはProviderに対して、ProviderにあるUserのリソースにアクセスする許可をUserからもらうように依頼する。

OAuthのフロー(3)
ProviderはClientがUserのリソースにアクセスすることを許可するかどうかをWebページにて確認する。

OAuthのフロー(4)
UserはClientがProviderにあるUserのリソースにClientがアクセスすることを承認する。

OAuthのフロー(5)
Providerは一時的なAccess Tokenを発行する。

OAuthのフロー(6)
Userは一時的なAccess TokenをClientに入力する。

OAuthのフロー(7)
ClientはProviderに有効なAccess Tokenを発行するように依頼する。

OAuthのフロー(8)
Providerはリソースアクセス用のAccess TokenとAccess TokenをリフレッシュするためのRefresh TokenをClientに渡す。

OAuthのフロー(9)
期限時間内はもらったAccess Tokenで持って、Providerと通信をして、Userリソースにアクセスする。
また、Userに対してClientの機能を提供する。

OAuthのフロー(10)
期限時間が過ぎたら、Refresh Tokenを使って、Providerに対して、新しいAccess Tokenを発行するように依頼する。

資料はこんな感じです。


最後の(9)と(10)はまだ検証していないので、ツッコミお待ちしております。

2011年8月10日水曜日

『アジャイルサムライ』読書記録 - 7章 - 見積もり:当てずっぽうの奥義

単なる読書メモなので、あまり読んでもおもろくないと思う。

まあ、SIerはそれをずっと維持し続けようとするから、あとで火を噴く訳だな。
開発なんてやってみなければどれくらいのスピードでできるかわからない。

  • 今後の計画をたてられる
  • 見積もりは当てずっぽうだという前提を踏まえている
  • ソフトウェア開発の複雑さを認めている


このプロジェクトをやり遂げられそうなのか?!は非常に重要なところだと思う。

できないものをできるとかいうと後々痛い目を見そうだし。

7.2 ピンチをチャンスに


  • ストーリーそれぞれを互いに相対的なサイズで見積もる
  • ポイントをもとにして進捗を追跡する



相対的な見積もりについて

ポイントについて

見積もりは確実なものでないとわかっているので、日数でなく、これくらいみたいな感じで見積もっておいたほうがよいってことかな。

SIer「このプログラム何日かかる?」
プログラマー「わかりませんが、この前の仕事の5割増ですね。」
SIer「つまり3日だね?」
プログラマー「どうですかね?この前の仕事は確かに2営業日でできていますけどね。まあ、たぶんこの前の仕事より5割増くらいだということですね。」
SIer「つまり3日だね?」
プログラマー「いや、分かりません。」
SIer「この前のは2日でできたのだから、それの5割増だから、3日だろう。」
プログラマー「どうですかね?この前の仕事は確かに2営業日でできていますけどね。まあ、たぶんこの前の仕事より5割増くらいだということですね。」
SIer「つまり3日だね?」
…以下続く

見積り技法


三角測量

ストーリーの一覧から1回のイテレーションの期間に収まりそうなサイズのストーリーを大中小選び出す。
選び出す際の視点。
  • 論理的なグループ分けができる
  • エンド・トゥ・エンドになっている
  • プロジェクトを象徴するストーリー

スパイク

今まで経験したことのないようなストーリーに対する見積もり方法。
タイムボックス化して数日以内でストーリーを見積もれる程度に様々な調査を行う。

プランニングポーカー

開発メンバーが一つ一つのストーリーを自分自身で見積もる。
その結果をメンバー同士で共有する。
一致していたら、その見積もりにする。
異なっていたら相違について話し合って見積りを出す。

重要なポイントは話し合いがあること。
プランニングポーカーは投票システムではないこと。
ストーリーのサイズは小さくする。(1、3、エピックを表すのに5を使う。)



2011年8月8日月曜日

仕様化テストの話をしようか

今日は仕様化テストのお話


先日、Androidテスト祭りで取り上げましたが、仕様化テストというテスト技法(?)があります。

簡単に仕様化テストとは何かをまとめるとこうなります。

ソフトウェアの仕様を調べ、その結果に基づいてテストを書くこと

マイケル・C・フェザーズ著、ウルシステムズ株式会社監訳、『レガシーコード改善ガイド』(翔泳社、2009年)、p.200


これは仕様のよくわからないアプリケーションないしはメソッドに対して仕様を明らかにすることができる技法なので、確実に覚えておきたい技法です。

というわけで、今回は画面サイズというなんかAndroidではちょっと嫌な部分について仕様化テストを書いてみようと思います。

まずはActivityを作ります。
これはHello, Android状態のアプリです。

MikeCountDown.java

public class MikeCountDown extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.mike_countdown);
    }
}


レイアウト頑張ります。


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
    <LinearLayout
      android:id="@+id/view_position"
      android:layout_height="fill_parent"
      android:layout_width="fill_parent"
      android:layout_marginLeft="5dip"
      android:layout_marginRight="5dip"
      android:layout_weight="3"
      android:orientation="vertical">

<!-- 省略 -->

       <SeekBar
         android:id="@+id/seekBar"
         android:max="15"
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
         android:paddingLeft="25dip"
         android:paddingRight="25dip"
         android:layout_marginBottom="25dip"
         android:thumb="@drawable/thumb"/>

<!-- 省略 -->

    </LinearLayout>
</LinearLayout>


さて、ここでお題

SeekBarの横幅の大きさを求めよ


ここで『リファクタリング―プログラムの体質改善テクニック』の一言が思い浮かぶわけです。

デバッグは最悪、テストはまだいい


微妙に間違っていると思いますが…

つまり、やつの大きさを求めるためにデバッグモードで起動するのは最悪ということです。
ではわからないものにどう対処するか?
わからないものには、テストを書きましょう。

MikeCountDownTest.java

public class MikeCountDownTest extends ActivityTestCase<MikeCountDown> {
    
    private Activity activity;
    
    public MikeCountDownTest() {
        super("org.mikeneck.android.countdown", MikeCountDown.class);
    }
    
    @Override
    public void setUp() throws Exception {
        super.setUp();
        activity = getActivity();
    }
    
    public void testGetWidth() {
        View view = activity.findViewById(R.id.seekBar);
        assertEquals(1, view.getWidth());
    }
}


テスト結果


落ちたので、大きさを取得することができました。
というわけで、これが通るようにテストを書きなおします。

MikeCountDownTest.java

    public void testGetWidth() {
        View view = activity.findViewById(R.id.seekBar);
        assertEquals(464, view.getWidth());
    }


テスト結果

はい、見事にテスト通過しました。

これでエミュレーターの仕様を知ることができました。
それと同時に、動かすことのできる仕様を手に入れることができました。

このような感じで現状の仕様にあわせてテストを書いていって、仕様書にしてしまう手法を仕様化テストといいます。

今テストがないのに、どうやってテストを入れればいいのかお悩みの皆さん、この手法でテストを挟み込んでいきましょう。


2011年8月7日日曜日

AndroidのViewのisEnabledあたりをTDDする。

Androidを実際にTDDしてみようと思う。

お題のアプリは『作りながら覚えるAndroidプログラミング』(ソフトバンク・クリエイティブ)という本のStep.6のカウントダウンタイマーを作るというやつです。

本に記載されているコードはこんな感じです。

public class CountdownTimer extends Activity {

    // 一部省略

    Button startButton;

    Button stopButton;

    SeekBar seekBar;

    public static void countdown(int counter){
        // 一部省略
        if(counter != 0) {
            stopButton.setEnable(true);
            startButton.setEnable(false);
            seekBar.setEnabled(false);
        } else {
            stopButton.setEnable(false);
            startButton.setEnable(false);
            seekBar.setEnabled(true);
        }
    }
}


まあ、counter0でなけば、
  • startButtonが触れなくなる。
  • stopButtonが触れるようになる。
  • seekBarが触れなくなる。
ですし、counter0であれば、
  • startButtonが触れなくなる。
  • stopButtonが触れなくなる。
  • seekBarが触れるようになる。
となります。

残年なところはこれActivityの子クラスなので、テストが半端なくめんどくさいのです。

というわけで、こういった操作を行うAndroidPOJOなクラスがあると考えてテストしながら作ってみたいと思います。



いや、そもそもViewってテストできるんだっけ?


これは非常に不安なので、不安を解消するためにテストを書いてみました。


public class CountDownControllerTest extends AndroidTestCase {
    public void testCanViewBeToggle() {
        Button button = new Button(getContext());
        assertTrue(button.isEnabled());
        button.setEnabled(false);
        assertFalse(button.isEnabled());
    }
}


テスト結果



グリーンですね。
大丈夫そうです。

ではおもむろに、期待する振る舞いを書いてみましょう。

CountDownControllerTest

private CountDownControllerTest controller;
    private Button startButton;
    private Button stopButton;
    private SeekBar seekBar;
    
    @Override
    public void setUp() thrwos Exception {
        super.setUp();
        controller = new CountDownController();
        startButton = new Button(getContext());
        stopButton = new Button(getContext());
        seekBar = new SeekBar(getContext());

        controller.startButton(startButton);
        controller.stopButton(stopButton);
        controller.seekBar(seekBar);
    }

    public void testSetProceeding() {
        controller.setProceeding();
        assertFalse(startButton.isEnabled());
        assertTrue(stopButton.isEnabled());
        assertFalse(seekBar.isEnabled());
    }


CountDownControllerというものがあって、それが画面のViewの操作に関する責務を担うものとします。それらのViewは外(ここではActivityを想定)から与えられるものとします。そして、操作メソッドはsetProcessing()とします。

最初の段階のCountDownControllerの実装は次のとおりです。

CountDownController

public class CountDownController {
    public void startButton(Button startButton) {
    }
    public void stopButton(Button stopButton) {
    }
    public void seekBar(SeekBar seekBar) {
    }
    public void setProgressing() {
    }
}


テスト結果

まあ、赤ですよ。
そりゃそうですね。何もしていませんから。

ViewというかButtonとか、SeekBarに対して仮の実装はできないので、おもむろに本実装をしてしまいます。

CountDownController
public class CountDownController {

    private View startButton;

    private View stopButton;

    private View seekBar;

    public void startButton(Button startButton) {
        this.startButton = startButton;
    }

    public void stopButton(Button stopButton) {
        this.stopButton = stopButton;
    }

    public void seekBar(SeekBar timeBar) {
        this.seekBar = seekBar;
    }

    public void setProgressing() {
        startButton.setEnabled(false);
        stopButton.setEnabled(true);
        seekBar.setEnabled(false);
    }
}


テスト結果

おっ!グリーン!きましたね。

では、もうひとつのケースを追加しちゃいましょう。

CountDownControllerTest

public void testEndProcessing() {
        controller.endProgressing();
        assertFalse(startButton.isEnabled());
        assertFalse(stopButton.isEnabled());
        assertTrue(seekBar.isEnabled());
    }


もちろん、仮実装します。コンパイルエラーをなくすための。

CountDownController

public void endProcessing() {
    }


テスト結果


ハイ♪ハイ♪ハイ♪ハイ♪ハ~イ♪レッドですよ~♪

というわけで、おもむろに本実装をします。
CountDownController

public void endProcessing() {
        startButton.setEnabled(false);
        stopButton.setEnabled(false);
        seekButton.setEnabled(true);
    }


では、テスト実行。

いいね、いいね!グリーン。いいね。

じゃあ、今度は仕様に即してテスト書こうか。

counterという値に1を設定した場合はsetProcessingと同じ結果になるように、counterという値に0を設定した場合はendProcessingと同じ結果になるようにします。

まずは、counter1の場合。

CountDownControllerTest

public void testCountDown() {
        controller.countDown(1);
        assertFalse(startButton.isEnabled());
        assertTrue(stopButton.isEnabled());
        assertFalse(seekBar.isEnabled());
    }


仮実装(コンパイルを通るだけ)にした上で、テストを実行



もちろん落ちるので、仮実装します。
CountDownController

public void countDown(int counter) {
        setProcessing();
    }


テスト結果


次にcounter0の場合。

CountDownControllerTest

public void testCountDownOnEnd() {
        controller.countDown(0);
        assertFalse(startButton.isEnabled());
        assertFalse(stopButton.isEnabled());
        assertTrue(seekBar.isEnabled());
    }


テスト結果


落ちるので、本格的かつシンプルに実装する。

CountDownController

public void countDown(int counter) {
        if(counter == 0) {
            endProcessing();
        } else {
            setProcessing();
        }
    }


そして、テスト

グリーン。

int0とか1というきわどい値をやっているので、-1をやらないわけがない。

CountDownControllerTest

public void testCountDownOnException() {
        try{
            controller.countDown(-1);
            fail();
        } catch(IllegalArgumentException e) {
            assertTrue(true);
        }
    }


そしてテスト

すぐさま期待する動作になるように実装。

CountDownController

public void countDown(int counter) throws IllegalArgumentException {
        if(counter == 0) {
            endProcessing();
        } else if(counter > 0) {
            setProcessing();
        } else {
            throw new IllegalArgumentException();
        }
    }


そして、すぐテスト

グリーン!( ゚Д゚ノノ☆パチパチパチパチ

このあと、ユーザーコード(つまり開発者が使うコード)のためにインターフェースを作成したいのだけど、それはみなさん考えてみてください。

こういう感じでAndroidでもTDDができるので、しかもAndroidPOJOでもTDDができるので、ぜひとも取り入れてみてはいかがでしょうか?

Androidテスト祭りでTDD晒してきたよ

Androidテスト祭りが開催されました。


ご来場いただいた皆様、USTをご覧になった皆様、および講演を引き受けてくださった皆様、大変ありがとうございました。
この場を借りて御礼申し上げます。

また、忙しい中、準備に奔走したスタッフの皆様お疲れさまでした。

当日の模様ですが、Togetterなどで少しまとめられております。

@sakura_bird1 のAndoroid テスト祭り実況

あと、手前味噌ですがこちらでもまとめています。

さて、恥ずかしながら、私も拙いスキルを晒して参りました。

プチライブコーディングとかはABC2011Summerにて晒していたし、Android埼玉支部の支部会でも晒してきたので、それほど緊張はしていないつもりでしたが、やはり70人、UST150人を前にしてのTDDはプレッシャーがありますね。全然スピードが悪かったと思います。

まあ、お題はいつものアレです。

そう、ボーリングですね。

比較的面倒なロジックな割にはルールは結構知られているという面白い題材です。

まあ、実際にはTDD以外にも仕様化テストの話もして参りました。
仕様化テストの話は明日にでも書きます。

資料は次のとおりです。



きっと、いろいろな方もブログを書かれると思いますので、私なりに簡単にまとめました。


KeyNote :: より効率的に開発するために

井芹 洋輝(TDD研究会) 太田 健一郎(TDD研究会)


いや、これはレベルが高かった。
単純に言うと、Activityにビジネスロジックを書くな」ということですね。

ゲームでもなんでもいいんですが、アプリケーションの中心となる機能(ゲームで言えば、ボールが当たるとか、点数を計算するなど)をActivityに書いてしまうと、途端にアプリケーションのテストができなくなって、品質を担保できなくなるということが話しの中心でした。
そして、テスト容易性を考慮しつつアプリケーションの設計を行おうというものです。

結構内容は高度なもので、ボリュームもたくさんだったため、ふたりとも早口でした。
というわけで、レベルが高いかな…などと思いました。

ちなみに、二人に聞いたところ、「妥協を一切しなかった」とのことです。

Android製品テストマップ

鈴木 利彦(株式会社ベリサーブ)


え~っと、まず、すんませんした!…m(_ _)m
どうやら、私が太田さんのノパソの操作を誤ったらしく、スクリーンが途切れてしまいました。
その後も回復がうまく出来ず、snskさんに応急処置をしてもらいました。
いや、ほんと、まじすんませんした。

話は端末の品質の話から、テスト観点の話など多岐にわたりました。

印象的だったのは、今後品質の良くなるメーカーの特徴を述べていたところで、社名は伏せられていましたが、BTSに登録したイシューに真摯に対応している端末メーカーの品質が向上しているとのことです。

品質を向上させるためには日々少しずつ改善していく必要があり、それにちゃんと取り組んでいるメーカーにはノウハウが溜まっていくであろうという話でした。

Android受け入れテストガイドライン

生路 茂太(株式会社ACCESS)


製品のQAに関するお話です。
フィードバックされた意見をどのようにして次のリリースに活用するかといったあたりの分析の仕方が述べられていました。


Android向けUIテスト自動化ツール「Scirocco」のご紹介

立花 優人(株式会社ソニックス/ファウンダー スマートデバイスソリューション事業部 アーキテクト)


UIテストツール「Scirocco」の紹介とデモです。

通常、UIテストをJUnit形式で書こうとすると、10行以上のテストコードになりますが、Sciroccoを使うとほんの2~3行でテストコードが書けるようになります。
UI周りのテストを書く場合には非常に参考になりますね。
Scirocco自体はオープンソース「Apache2.0ライセンス」で提供されています。

なお、ソニックスさんはこれほどよいツールを何故無償提供したかというと、「社会貢献」だそうです。
大人ですね~。

SQLite 周りのテストをしよう

@ussy00 (Androidテスト部/株式会社ヌーラボ)


uss00さんがすんごいのを作ったそうです(棒
自分の番が次なので内容をほとんど覚えていない…

とりあえず、SQLiteのテストをやり易くするためにライブラリーを作ったとのことで、
GitHubにて公開されています。

招待講演 :: テスト可能なUI設計パターン

MVCとかMVPといった責務の役割をAndroidにも適用しようというお話です。
実はAndroidの開発になってからJavaのプロダクトコードの品質が7~8年前のレベルに戻ってしまったそうです。
MVCとかMVPというのは古い、使い古された考えですが、目的は責務の分割です。しかし、現状AndroidのアプリではActivityにモデルロジックが書かれてしまったり、Viewの役割をもつコンポーネントにモデルが記述されたりしていて、責務分割が忘れられている傾向があるようです。
原因のひとつはAndroid入門書の本をそのまま写経することなどがあったりします。(これは悪いことではない)。
ただ、入門書はその本としての可読性を優先するために、ソフトウェアとしての品質を犠牲にしています。
なので、責務分割は必ずして品質を担保しようということでした。
ちなみに紹介されたAndroid-Bindingは試してみたいと思います。

クロージング :: これからのアンドロイドテストの話をしよう

宮田 友美 (Androidテスト部部長/株式会社オープンストリーム)


これからのテストの話しです。
これまでのテスト部は「どうテストをするか」にフォーカスを当てていましたが、今後は「ユーザーは何を求めているのか」という方向にも進んでいくという宣言がなされました。
また、テストの自動化も積極的にやっていきたいし、testterのコミッターももっと増やしたいとのこと。

あと、私事でしたが、お子様が3/9予定日だということです。おめでとうございます。


魂心会もとい懇親会

いろいろな方とお話…しませんでした…orz
LTの準備全然やっていなかったので…

というわけで、Groovyの宣伝だけしました。
他のLTで、一番衝撃的だったのは、某G**gleの社員さんでNative Driverの開発者の方がLTしたことでしょうか。
いや、むしろメインセッションでやってほしいくらいでした。

懇親会ではKORODROIDさんや、デ部の方々とご挨拶致しました。
また、Android埼玉支部ではいつもお世話になっているねこすけさんともご挨拶いたしました。

今後ともよろしくおねがいします。

まあ、こんな感じですが、こんごともAndroidテスト部よろしくお願いします。