ラベル Test の投稿を表示しています。 すべての投稿を表示
ラベル Test の投稿を表示しています。 すべての投稿を表示

2012年12月28日金曜日

Spockで例外のテスト

年末ということで広島に来ているみけです。

Spockとは


まあ、ググってください。


例外のテストがうまくいかなんだ…


したがってSpockはJUnitで@RunWith (Theories.class)でやるようなテストを


非常に見やすい形で、かつ型安全に実行できるわけです。


そこで、例外のテストを今度は書いてみることにしました。



テストを実行すると…




落ちたよ(´・ω・`)

Spockは例外を扱うようなテスト書けないのか…残念無念orz


しかたがないので@RunWith (Theories.class)使ったよ…


というわけで、Spockを諦めてJUnitへ…

なんか、@RunWith (Theories.class)使うと、

@DataPointsでデータを指定できるのですが、

なんか配列を強制されてるっぽいので

個人的にはイケてない気がする…
(まあ、便利ですけど、それとも僕の書き方が悪いのか…)
(求むツッコミ!)




で、テスト実行、テストクリア!




ということで結論…




と、そこでいろふさん登場。



ふむ、thrown!そんなシンタックスがあるのか!

というわけで、Spockのページを読むと、確かにあった





Spockで例外のテスト


Spockのページを参考にテストを書きなおしてみると、

こんなんなった。





そこでテスト実行。




通った!通ったよー!


結論


出来上がったソースを見てみると、

境界値テストとかのデータパターンを書いてテストするのに

条件と期待値が一覧形式で読むことができるので、

Spockはかなりいいですね。


Spockのサンプルに載っているようなテスト





実は僕、これの何が嬉しいのかわからなかったんですよ。


だけど、@RunWith (Theories.class)を書いた後に、


Spockで書きなおしてみると、Spockのありがたさが非常によくわかります。


真の結論











ということなので、いろふさん早くSpockについてブログ書いて下さい。








2012年12月10日月曜日

CDIについて書こうとしたら、JSRを読み始めて、結果JSR330を実装してみようとしてみた件について

Advent Calendarをダブルブッキングしたミケです。

こっちはJavaEE Advent Calendarの方の記事になります。

昨日は@yamadamnさんの「WebLogic Server 12.1.1 on Mac OS X」でした。


Contexts and Dependency Injection (CDI/JSR-299)


今日はJavaEEのCDIの話をしようと思いましたが、

JavaEEとかよくわかってないんで、

いろいろと調べているうちに、

全然別のDependency Injection(JSR-330)の方向に進んでいってしまいました。

まあ、すこしは関係あるので、いいかな…


一応、JSR-299とJSR-330の簡単な関係を…


元々のJSR-299に対してgoogleのボブ・リーがこれじゃちょっとみたいな感じで

ロッド・ジョンソンとともに提案したのがJSR-330ですね。

で、JSR-299はJSR-330の上のレイヤーで動作するという感じになっています。

こちらを参考


tckを通してみよう


さて、CDIとDIを使ってなんか遊んでみようと思って、

Maven Repositoryを漁っていたら、

javax.injectのtckを発見しました。

ということで、急遽JSR-330を実装してみることにしました。

(終わってないけどね…)


プロジェクトの作成


恥ずかしながら僕はpom.xmlが書けないので、

build.gradleはこんな感じです。





tckの実行


最初、tckのソースも読まずにテストをやろうとして、

いろいろと粘っていましたが、

テストはこうやって書けよとtckのjavadoc

書いてありました。





Carというクラスはtckの中で書かれているインターフェースです。

で、tckにCarのインスタンスを渡せば、

テストしてくれるようです。


TDD?


テストを先に書いたので、org.mikeneck.inject.Injectorなんてのは存在しません。

だから、コンパイルが通るように適当に実装を書いています。





まあ、当然ですが、ヌルポです。




実装についての説明もtckに書いてあります


tckのjavadocを読むと、

Carクラスの実装はConvertibleクラスだそうです。

また、他にもインターフェースと実装の指定が書いてあり、

それらを参考にしながら実装することになるようです。


昨日からやり始めたことなんで、

まあ、ちょっとやそっとではできなそうですね。

適当に時間の合間を縫って実装してみたいと思います。

(かなり難しい気がする…)


なお、Google Code上にあるWikiによると、

tckを通った実装が、5つあるようです。



おしまい


なんかJavaEEとほとんど関係のない話になってしまいました。

すんません。


きっと、明日(2012/12/11)記事を書いてくださる@matsumanaさんが

話をいい方向に戻してくださると思います。

2012年1月3日火曜日

はじめてGrailsをさわるオッサンがGrails2.0でDomainを作ってみた


An elder engineer, new for Grails, got involved into a trouble on unit testing of Grails2.0 domain classes.

Grails2.0



昨年末にGrails2.0がリリースされました。
というわけで、正月のお休み期間を利用して触ってみることにしました。

とはいえ、実は初めてGrailsを触るので、『Grails徹底入門』をお手本に写経することにしました。








Grailsというのは、SpringHibernateGroovyを元にして作られたJVM上で動作するWebアプリケーションフレームワークです。
設定よりも規約に準拠することにより高い生産性を発揮することを目標に作られています。
また、GroovyとJavaの親和性は高く、既存のJava資産を有効に活用できるフレームワークでもあります。


Domain


あらまし


アプリケーションを作成する場合、まず問題がどういった要素が関係しているか分析を行います。
これらの要素と関連はWebアプリケーションとか、Androidアプリケーションとかそういったプラットフォームに依存することなく存在します。
こういった要素と関連のことをModelと言ったり、Domainと言ったりします。
GrailsではDomainという言葉で指しているようです。
(テキトーなオッサンなのでツッコミ( `・∀・´)ノヨロシク)

BookとPublisher


『Grails徹底入門』では本屋さんを模してアプリケーションを作成していますので、本(Book)とか、出版社(Publisher)とかそういった類のDomainを定義しています。
というわけで、内容をパクって参考にして、BookとPublisherの関係を書いてみました。


関係性


まあ、難しい関係ではありませんね。
Bookからみて、Publisherは…

  • 一つだけある。
  • 必ず存在する。

という関係があります。

逆にPublisherからみて、Bookは…

  • 複数ある。
  • 存在しないこともある。

という関係があります。

まあ、こういう話は、ER図の本とか、データベース設計の本などの方が詳しいので、そちらに譲りましょう。
というわけで、このドメインをGrailsで作っていきたいと思います。

ドメインクラスの作成


create-app


まず、プロジェクトを作成します。


$ cd workspace
$ grails create-app BookShop


これで、workspaceディレクトリの下に次のような構造をもつBookShopプロジェクトディレクトリが作成されます。


なお、プロジェクト名が違っていますが、そこは、ほら、その、なんというか、大人な感じに、つまりいい感じに読んでいって下さい。


create-domain-class


次に、ドメインクラスを作成します。


$ cd BookShop
$ grails create-domain-class Publisher
$ grails create-domain-class Book


すると、次のようにDomainクラスとTestクラスが作成されます。

  • grails-app
    • domain
      • bookshop
        • Book.groovy
        • Publisher.groovy
  • test
    • unit
      • bookshop
        • BookTests.groovy
        • PublisherTests.groovy


これらの自動生成されたクラスを編集してドメインを実装していきます。

あと、Orderとかいうクラスがすでに追加されていますが、そこも、ほら、あの、なんていうか、その、大人な感じで( `・∀・´)ノヨロシク。

ドメインクラスの実装


メンバーを与える


先ほど書いた図を元にドメインクラスにメンバーを与えていきます。

Publisher.groovy

class Publisher {

    String name

}


Book.groovy

class Book {

    String name

    String author

    int price

    Date releaseDate

    String isbn13

    String imageUrl

    String description
}


はい、ここまでは難しくありません。

関係に関するメンバーを追加する


で、これに先ほどの関係のメンバーを付与します。

まず、Publisherの方ですが、Bookを複数持ちますので、hasManyを用います。

Publisher.groovy

class Publisher {

    String name

    static hasMany = [books: Book]
}


これによって、PublisherクラスにはbooksというList<Book>クラスのメンバーを持つことになります。

同様にBookクラスにもPublisherへの関連性をもたせます。
BookクラスはPublisherクラスを唯一つ持ちますので、belongsToを用います。

Book.groovy

class Book {

    String name

    String author

    int price

    Date releaseDate

    String isbn13

    String imageUrl

    String description

    belongsTo = [publisher: Publisher]
}


これによって、BooksクラスにはpublisherというPublisherクラスのメンバーを持つことになります。

ちなみに、これはGroovyのうれしいところですが、

Groovy ではメンバーを記述するだけで、自動的に setter / getter を自動生成してくれる

という機能があります。


Plain Old Java Object : POJO みたいに、 Plain Old Groovy Object : POGOなんて呼ばれているとかなんとか…

制約 (Constraints) を追加する


さて、さきほど関係性だけを記述しましたが、これではまだ十分ではありません。

  • 0個存在するとか、1個は必ず存在するとかの制約 (Constraints) の記述がない
  • 本の名前がないとか、著者がないとか、価格がマイナスとかっておかしい
  • 出版社の名前がないのというのもおかしい

関係性に関する制約にとどまらず、基本的な事柄に関する制約条件が満たされていません。

というわけで、これらの制約を盛り込んで行きましょう。

Publisher

まずは Publisher の方からですが、こんな制約が必要でしょうか…

  • 会社名の空白は禁止
  • 会社名が他の会社とかぶってはいけない

まあ、実際には2の制約はないでしょうけど、まあここではそういうことにしておきましょうwww

このような制約を与える場合に使うのが、 constraints です。

では、 Publisher に制約を与えてみましょう。

Publisher.groovy

class Publisher {

    String name

    static hasMany = [books: Book]

    static constraints = {
        name(blank: false, unique: true)
    }
}


追加した部分はこんな感じです。

  • 会社名 (name) に対しては、空白 (blank) は、禁止 (false) 。
  • 会社名 (name) は、唯一 (unique) に決まること (true) 。

これだけでいいんですか?これだけでいいんです( ー`дー´)キリッ

制約に関するテストを書く


疑心暗鬼に陥っているときは、テストを書いて安心するのがよいらしいです。

PublisherTests.groovy

@TestFor(Publisher)
class PublisherTests {

    @Test
    void validateName() {
        def publisher = new Publisher(name: 'hoge')
        assert publisher.validate()
    }
}


とりあえず、名前のある会社は大丈夫だよねっていうテストを書いてみました。
ちなみに、コンストラクターのところにある記号、name: 'hoge'というのは、Groovy特有の書き方で、Javaで書くとこんな感じになります。


@TestFor(Publisher)
class PublisherTests {

    @Test
    void validateName() {
        def publisher = new Publisher()
        publisher.setName('hoge')
        assert publisher.validate()
    }
}


では、実行しましょう。

Grailsのテストサポート


Grails interactive


さて、テストを実行したいところですが、コマンドgrails test-app bookshop.Publisherと実行するだけです。

実行するだけなのですが、色々とコマンドを覚えるのが面倒ですね。

そこで、grailsのコンソールを起動して、そちらに任せてしまいましょう。

ちなみに、grailsのコンソールとテキトーに書いていますが、正確にはgrails interactiveというらしいです

grailsのコンソールは何がいいかというと、もしコマンドがわからなければ、Tabを押すだけで、何を入力するべきか表示してくれます。

起動


起動方法は簡単です。


$ grails


と入力するだけです。

こんな感じでgrailsのコンソールが立ち上がります。


$ grails
| Enter a script name to run. Use TAB for completion:
grails>


何をするんですか?


何をすればいいかわかりませんね。Tabを押して下さい。


いろいろとコマンドが表示されます。

自分のやりたいコマンドを選ぶだけで構いません。

そうだテストをしよう


今はテストをやりたいので、test-appコマンドを入力します。

でも、このコマンドはすべてのテストを実行するコマンドなので、若干不便です。

たった一つのクラスを変更しただけなのに、すべてのテストを実行するのは後々時間がかかって大変です。

そういうのはJenkins先生とかにお願いしましょう。

そこで、test-appまで入力して、Tabを押して下さい。


どういうテストができるのか一覧が表示されます。

やりたいのはPublisherのテストなので、それっぽいやつを途中まで入力して、


Tabを押します。


お、なんかいい感じに入力できるコマンドが指定されてきました。

Publisherも途中まで入力して、


Tabを押します。


勝手に補完してくれますね。これで、Publisherだけのテストを実行できます。

では、おもむろに実行!


成功ですね。

え〜、ここでも、テストの個数が若干異なっていますが、そこは大人の事情ということで…

ちゃんとPublisherのテストを書く


blankに対するテストを書く


さて、今のテストはただ単に成功するだけの条件を書いたので、あまり意味のあるテストではありません。

そこでnameのvalidationに対するテストを書いていきます。

  • namenullだったらエラーとなるか?
  • nameが長さ0の文字列だったらエラーとなるか?

まず、この二つは確実に抑えておきたいところです。

これについてテストを書いていきます。

PublisherTests.groovy

@TestFor(Publisher)
class PublisherTests {

    @Test
    void validateName() {
        // name が null の場合
        def publisher = new Publisher()
        assert publisher.validate() == false

        // name が '' の場合
        publisher = new Publisher(name: '')
        assert publisher.validate() == false

        // name が長さ1以上の文字列の場合
        publisher = new Publisher(name: 'hoge')
        assert publisher.validate()
    }
}


テストを実行。


はい、成功です。

uniqueに対するテストを書く


さて、Publisherの制約条件にuniqueというのがありました。

読んで字の如くで、同じ名前の会社が登録されていたら、エラーとするというものなのですが、データベースが必要になってきます。

面倒くさそうになって来ました。

そこでGrailsでは、Unitテストにおいてデータベースがなくてもキャッシュだけでテストを行えるような仕組みを提供しています。

mockForConstraintsTests(Class<T>, List<T>)を用いてテストを実行します。

PublisherTests.groovy

@TestFor(Publisher)
class PublisherTests {

    @Test
    void validateName() {
        def existingPublisher = new Publisher(name: 'exists')
        mockForConstraintsTests(Publisher, [existingPublisher])

        // name が null の場合
        def publisher = new Publisher()
        assert publisher.validate() == false

        // name が '' の場合
        publisher = new Publisher(name: '')
        assert publisher.validate() == false

        // name がすでに存在する会社の名前と一致する場合
        publisher = new Publisher(name: 'exists')
        assert publisher.validate() == false

        // name が長さ1以上の文字列の場合
        publisher = new Publisher(name: 'hoge')
        assert publisher.validate()
    }
}


では、テストを実行してみましょう。


テスト通りました。

締め


以上、gdgdな感じの紹介になりましたが、こんな感じでDomainを作っていきます。

本当はもっとハマリどころがあるんですが、その前段まででめちゃくちゃ長くなってしまったので、ハマリどころについては次回やります。

ん、インストール方法が載っていない?

zipをダウンロードしてパスを通してあげて下さい。


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年7月18日月曜日

Android Bazaar Conference 2011 Summer に行ってきました。

国内最大のAndroidに関するイベントAndroid Bazaar Conference 2011 Summer に参加してきました。

私自身は一般参加として登録しましたが、Androidテスト部の展示のお手伝いをしてきました。

当初、monkeyrunnerでテスト部で作成したTwitterクライアントを実機上で走らせるデモを行う予定だったのですが、準備不足マシンの調子が悪く、スムーズにデモを開始できなかったため、一人TDDデモをやってみました。

ネタはいつものアレです。


お題:ボウリングのスコアを算出するプログラムを作成せよ。

  • 条件
    • テストコードとソースコードを作成する。
    • 入力は配列もしくはjava.util.List<Integer>の形で与えられる。
    • ストライクの後には0が設定されるものとする。
    • ガーター、ミスはともに0が設定されるものとする。

というやつです。

これを声を大きくしながら実演すること6~7回。
さすがに疲れましたが、
  • TDDというのを聞いたことがあったが、実際にこうやるものだと思わなかった。
  • 非常に面白かった。
  • 今度会社でやってみようと思う。
といった声をきかせていただいたので、非常に満足な結果になったと思います。

実はこれ、Androidというプラットフォームに関係の無いネタだったのです。
しかし、ソフトウェアの特性上、何らかの判断などを行うので、表示機能やセンサー周りでないモデル・ロジックの部分での開発にTDDを組み込んでいくというのは避けて通れません。
その点でTDDにより常にフィードバックを受けて、自信を持って開発していけると、最終的に(保守的にも新規機能追加に関しても)品質の高いソフトウェアを開発できると思います。



個人としてはAndroid Developers' Club (デ部)のゆかいな仲間たちによる「Android3.0/3.13.2 HoneycombとAPI解説」を聞いてきました。

最近人気のあるArduinoなどのサポートが強化されておりますし、各種コントローラーをつなげることもできるようになっているあたりは非常に面白いと思います。
また、3.0以降に追加されたFragmentについても簡単ですが解説されていて、実際触ってみたいと思いました。

…エミュレーターが重くなければなw

2011年6月13日月曜日

簡単なActivityInstrumentationTestCase2の書き方

Androidのプログラムの作成方法については、いろいろなサイトで有用な情報が得られます。
しかし、テストとなるとあまり情報が多くありません。

というわけで、
簡単なActivityInstrumentationTestCase2<T extends Activity>の書き方を
記事にしました。

今回のテスト対象のアプリケーションはこんな感じです。
概要
  • EditViewButtonTextViewで構成されている。
  • EditViewに文字を入力して、Buttonを押すと、TextViewに「Hello, + [EditViewの文字]」が表示される。



まあ、百聞は一見にしかずなので、画面イメージ出しましょう。
これが最初の画面です。


そして、これに文字を入れて、ボタンを押すとこうなります。






















では、早速テストを書きましょう。
まずは、コンストラクターとsetUp()から。


class HelloAndroidTest
        extends ActivityInstrumentationTestCase2<HelloActivity>{

    private Activity activity;

    private Instrumentation instrumentation;

    public HelloAndroidTest(){
        super(HelloActivity.class);
    }

    @Override
    public void setUp() throws Exception {
        super.setUp();
        activity = getActivity();
        instrumentaion = getInstrumentation();
        setActivityInitialTouchMode(false);
    }        
}


まず、ここのポイントは
  • コンストラクター
  • setUp()中にあるActivityInstrumentationTestCase2#getActivity()
  • 同様にsetUp()中にあるActivityInstrumentationTestCase2#getInstrumentation()
ですね。

コンストラクターではテスト対象のアクティビティ・クラスをActivityInstrumentationTestCase2に渡してやります。「総称型(ジェネリクス)でクラスを指定しているんだから、なんでわざわざクラスを通知してやる必要があるの?」という声も聞こえそうですね。すこしだけ解説すると、タイプパラメーター<T>というのは、ただただタイプとして機能するだけで、java.lang.Class<?>オブジェクトのような機能や、役割を一切持ちません。

したがって、

        Class clz = T.class;

とか、

        boolean isInstance = object instanceof T;

とか、

        T object = T.newInstance();

などはすべてコンパイルエラーになります。

これよりも詳しい説明は私が説明するよりも他にもっと優れた解説があります。Java総称型メモなどを参考にしてください。

話を元に戻すと、ActivityInstrumentationTestCase2で、テスト対象のアクティビティを起動するために、そのクラスの情報が必要なわけですね。さて、これジェネリクスで指定したクラスと異なるクラスを渡したらどうなるのか?わかりませんねぇ。あとでやってみましょう。まぁ、予想できる結果として、この後の解説で述べる#getActivity()java.lang.ClassCastExceptionが発生するように思います。

setUp()中にあるActivityInstrumentationTestCase2#getActivity()はActivityを起動して、参照を返すメソッドです。これにより、テストコードはテスト対象のアクティビティをそのコンテクスト上で起動することが可能になります。テストはそれ自体でひとつのアプリケーションですが、テストアプリケーションとは別のスレッド上にテスト対象のアプリケーションが起動するのです。これをsetUp()に置くことで、毎回のテストがアクティビティを起動した状態で実行できます。

最後に、setUp()中にあるActivityInstrumentationTestCase2#getInstrumentation()です。このInstrumentationは、アクティビティの起動とは何の関係もありませんが、アクティビティのUIスレッドと同期をとるような場合や、UIに対して操作を行う場合に必要になるオブジェクトです。今回のテストアプリケーションでも使いますので、最初にその参照を取得しておきます。

では、テストコードをどうぞ。


    public void testPressButton() {
        // EditText にフォーカスを当てる ---- (1)
        EditText editText = (EditText)activity.findViewById(
                orz.mikeneck.hello.R.id.edit_text);
        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                editText.requestFocus();
            }
        });
        // UIとの同期をはかる ---- (2)
        instrumentation.waitForIdleSync();
        // UIにキーを送る ---- (3)
        sendKeys(KEYCODE_SHIFT_LEFT);
        sendKeys(KEYCODE_A);
        sendKeys(KEYCODE_N);
        sendKeys(KEYCODE_D);
        sendKeys(KEYCODE_R);
        sendKeys(KEYCODE_O);
        sendKeys(KEYCODE_I);
        sendKeys(KEYCODE_D);
        // 前提条件の確認 ---- (4)
        assertEquals("Android", editText.getText().toString());
        // Button をクリックする ---- (5)
        Button button = (Button)activity.findViewById(
                orz.mikeneck.hello.R.id.button);
        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                button.performClick();
            }
        });
        // UIとの同期をはかる ---- (6)
        mInstrumentaion.waitForIdleSync();
        // TextView の値をテストする ---- (7)
        TextView textView = (TextView)findViewById(
                orz.mikeneck.hello.R.id.edit_text);
        assertEquals("Hello,Android", textView.getText());
    }


大雑把な解説ですが、
  • EditTextにフォーカスを当てます。これはUI上の操作に当たるので、Activity#runOnUiThread(java.lang.Runnable)を使用します。
  • UIとの同期をはかります。別スレッド(アクティビティ)との同期はInstrumentationwaitForIdleSync()を用います。
  • UIにキーを送ります。android.view.KeyEventをstatic importしておいてください。
  • EditTextにキーを送ったので、それが反映されていることをテストします。想定される値は「Android」です。
  • Buttonをクリックします。これはUI上の操作に当たるので、Activity#runOnUiThread(java.lang.Runnable)を使用します。
  • UIとの同期をはかります。別スレッド(アクティビティ)との同期はInstrumentationwaitForIdleSync()を用います。
  • Buttonをクリックしたので、仕様通りにTextViewのテキストが変更されているかテストします。想定される値は「Hello,Android」です。
となります。

ポイントは
  • UI上の操作は必ずActivity#runOnUiThread(java.lang.Runnable)を使用する。
  • UI操作後はInstrumentationwaitForIdleSync()を用いる。
です。

#runOnUiThread(java.lang.Runnable)を使わないとUIが操作できないのは、他のアプリケーションからいくらでも値を変更するなどの改変が行われてしまうからというセキュリティ的な側面があるのかと思われます。(Javaそんなに詳しいわけではないので、そのあたりを突っ込んでくれる人、大募集)

二番目のInstrumentationwaitForIdleSync()ActivityInstrumentationTestCase2の真骨頂です。UIスレッドとの同期をはかりスムーズにテストを実行出来るようなオブジェクト類が提供されており、それがActivityInstrumentationTestCase2です。別スレッドのUIと同期するこの能力はテストの実行においてかなり強力な機能です。



こんな感じで、非常に簡単なテストでしたが、みなさまもUI上で不具合を発見したら、なるべくテストコードを書いて、それらを再現できるようにしてみてください。きっと、素晴らしいUIをもつアプリケーションが作成できると思います。

2011年6月7日火曜日

第2回テスト駆動開発入門読書会(ペアプロ写経大会)に参加してきたよ

第2回テスト駆動開発入門読書会(ペアプロ写経大会)に参加してきました。

会場は早稲田大学で、実は人生初早稲田。
会場についたのは30分前でしたが、
まだ誰もいませんでした…orz

さて、開始からですが、
微妙に手際が悪くて10分遅れでスタート。

席の配置を円形に直して、
適当に座ったとなりの人とペアを組むという感じでスタートでした。

オレとペアを組んだのは組込み系に勤める今年一年目の社員の方で、
ノパソに入っていたEclipseが完全にまっさらで、
とてもビックリしました。
(よくある…)

なんか真新しいEclipseにこれから汚いコードをゴリゴリコードを書いていって
いいのか、やや戸惑いつつも、まあいずれ書くんだから
新鮮なテストが書かれていればいいじゃんという感じでやり始めました。

やったのは
  • ペアプロ
  • TDD本を一章から写経する
  • Kent Beckの本の内容を読みつつ、半分納得する。(三角測量はオレが簡単に説明)
  • 2~3章ごとに交代する
という感じです。

オレが最初にナビゲーターになりましたが、
とりあえず、操作がおぼつかないので、
「Ctrl + N」とか言いながら(ゴメンね…とはあまり思っていない)、
ショートカットを覚えてもらいました。

でも、やはりペアの方は若いので、
ショートカットをどんどん覚えるし、
少し羨ましい感じがしました。
(オレのように老いると、物覚えが悪くなって頑固になるのよ…)

2章を終わったあたりで、
ドライバーとナビゲーターを交代しました。

いつものボッチのような感じで、
ブツブツつぶやきながらコードを書きました。
なるべくナビゲーターに身を委ねつつ。

で、Dollar#equals(java.lang.Object)メソッドの
オーバーライドで、
どうしても気になって仕方がないので、nullのパターンを実装してしまいましたが、
これは本質出来ではありませんね。
ToDoリストに記載しておいて、
TDDではないテストコードを記述するときに、別途実装するべきでした。

この辺はナビゲーターの方にToDoに記録してもらいたいのですが、
ナビゲーターの真に難しいとろこは、
二人してコーディングにのめり込んでしまうあたりですね。

この点については、残り20分程度の感想戦で、
議論させていただきました。

頂いたアドバイスとしては、
  • ToDoに記録しておくかどうかはともかく、ポイントはドライバーとナビゲーターが対話をすることである
  • ナビゲーターが距離を少し置くためにポスト・イットを活用する
などがありました。

ぜひ、今後試させていただきたいと思います。

あと、気になった議論として、SIerの現場に同普及するべきかというのがありました。

たしかにSIのプロジェクトの現場ではペアプロする = 二人の稼働でひとり分の成果しかでない
と考えてしまうので、正直のところ導入には非常に慎重的な態度になります。

とは言うものの、SIerの現場にてよく見る光景で、
  • 御前レビュー会議 : えらい人だけが発言・指摘するだけの5~6人による1.5h程度のレビュー - 約7.5hの稼働の無駄
  • デバッグ、皆で考え込む症候群 : なんかうまく動かないが、どこが動かないかわからない(特に新人などが引き起こす)ので、なぜか2~4人が集まってきて2~3時間くらいあ~だこ~だ議論して、なんかよくわからないけど動いて、「あ~よかった」とか言って終りにしてしまうやつ - やっぱり7.5hの稼働の無駄

こう言うのがあるわけで、
意外と無駄に時間を過ごしているパターンは多いわけです。

だったら、無駄な7.5hを二回に分けてペアプロをするだけで、より進むと考えれば、
御前レビューとか、謎の動かない症候群よりも効果はあるように思います。

ま、現場のエロイエライプロマネの方々は、
こんなことを言っても(゚Д゚)ハァ?って顔しますがね。


次回は再来週あたりを想定しているとのことです。
ので、今回参加されなかった皆さんも参加してみてはいかがでしょう。

全然読書会の雰囲気が表現できていませんが、
全体的にはまったりしていましたよ。

2011年6月5日日曜日

97 things Every Project Manager Should Know を読んでみた(2)

Avoid Whack-a-Mole Development

モグラたたき開発から逃げろ


開発する早さの評価に関するエッセイ。
日本のSIerではバーニーさんの方が評価される傾向にありますね。
ものづくり日本という標語が盛んに叫ばれたりしています。
でも、なんか年末になるといらない工事とかがあったりして、
作りっぱなし日本という標語が適切な気がしなくもないです。
だからこそ、読んでほしい一節です。

ちなみに、前回と同じく、翻訳の詳しいところはいい加減です。
翻訳はノリが大事です。
多分怪しいところは満載なので、ツッコミ歓迎です。
それと、オレには突っ込まないけど、オレだけ知ってウッシッシな人はGoogle 翻訳にでもかけてください。

SOFTWARE PROJECT MANAGERS faces a lot of pressure to deliver fast. Time is of the essence. How can you get things done fast?



Imagine you have two programmers on your team, Bernie and Rob. Both are capable, have the same amount of domain knowledge, and have equal language skills. During development, you find that Bernie finishes his feature implementations much faster than Rob.



While Bernie focuses on getting the code completed quickly, Rob spends time writing code and then refactoring it. He names the variables and methods better. Once he gets things working, he breaks the code into smaller pieces. Now he writes tests to make sure each piece of his code does what he meant it to do. When he's reasonably satisfied, he declares the coding of that functionality done.




But assume you don't know these details. If you only look at the speed with which the functionalities are declared done, clearly Bernie is the better man, right?



A few weeks go by, and you demonstrate the features to your customer. As usual, the customer loves your completed features, but now wants you to change and improve them. You ask your developers to make those code alterations. When you take the new and improved functionality back to your customer, they try out the features that Rob implemented and are pleased with the changes.



Unfortunately, they discover something odd with the features that Bernie implemented. While Bernie has programmed in the new functions fine, now a few things that worked before don't work anymore. The customer marks these as defects, and you ask Bernie to fix them. The customer tests the features again. Now even newer, stranger things seem to be broken. What's going on here?



If you have a child, you know what is happening. Bernie has created a Whack-A-Mole application. Whack-A-Mole is a toy. Kids are given a wooden hammer to strike moles that pop up at random. It's fun for them to be surprised by which mole pops up next. However, fixing applications with broken code popping up at random places is not fun. It is frustrating, unpredictable, and it slows your product development. Bernie was sprinting initially, but he was running in the wrong direction.



While Rob appeared slower at the outset, he was actually creating superior code. His pace proved sustainable. The better quality of his initial code helped him make workable changes quickly. Plus, the test he wrote in the beginning gave him instant feedback regarding whether or not his new code was compatible with other parts of the application where the code was used.



When measuring time for a feature implementation, do not consider only the time it takes to write it in the first place. Add the time it takes to enhance, fix, and improve the code. Writing good quality code and tests takes time. It appears to be a short-term loss. However, it comes with a long-term gain.

Ask yourself if you want speed, or if you want to savor sustainable progress.



ま、このブログを読む人なんて、えらいプロジェクトマネージャーなんかいないだろうけどね。
それと、お客さんに同じ機能のテストをさせるなんて、どう考えたってムダでしかない。
ソフトウェアを作りっぱなしで、お客さんになんども同じことをさせるのはどうかと思う。