White Box技術部

WEB開発のあれこれ(と何か)

「テストを書く文化がないからテストがないんです」

それは怠慢だろうが!いい加減にしろ!!

じゃあテストコードはこのプロジェクトなくていいのか?と問えば、ある方がいいと思うと答えてくるが、 じゃあなぜ書かないのかと問えば、文化が・・・と答えてくる。

中には、画面で実際にオペレーションをしてテストをしているからテストコードの必要性は感じないと、 使い慣れたそろばんがあるから使い方の分からない電卓はいらない、というようなことを言い、 どんなバグが多いのかと問えば(動的型付け言語での)タイプミスデグレと回答が返ってくる。

不思議なのは、そういう人もテストコードがあることでそういうバグが防ぎやすいということを、 「知識として持っている」と言ったりするということです。

だからそれを防ぐためにテストコードを書こうよ!と言うと、

  • 今まで書いてこなかったから
  • プロジェクトがテストの書き辛い構造になっているから
  • テストを書く工数を貰っていないから
  • テストの書き方がわからないから

そして最後には、テストを書く文化がないから・・・

言わせて貰っていいでしょうか。

「だから!それは!!お前の怠慢だ!!!」

テストを書く文化()

そもそも何なのでしょうか、テストを書く文化って。

テストを書くことに文化が必要なのでしょうか?
書いたら「良くもこんなもの作りやがって!」と、袋叩きにでも合うのでしょうか?

そうだとしたら、早めにその環境から脱出することをお奨めします。

書いたらいいじゃないですか。
テストという概念を知っている、現代のソフトウェアエンジニアであれば。

テストを書きやすい環境

これならわかります。

プロジェクト内でテストコードが動作するように環境構築されているとか、 リポジトリにコードをプッシュすると自動でテストが実行され、その結果がチャットツールに通知されるとか、 進んだところではテストコードの書き方や指針があるかもしれませんね。

ですが、これがなかろうがテストを書くことはできますし、可能な範囲で環境作りを始めることもできるはずです。

工数の話

ちゃんと取ってください。リスクを認識している人には、リスクの説明と警告の責任があるはずです。

そもそもテストコードを書くためのライブラリがない

この状況であれば流石に怠慢とは言えないですね。申し訳ないです。

ですが、そのまま開発を続ける前にテストフレームワークのある言語への見直しをお勧めします。

もうわからないんです

別に今までテストがなかったコード全部に対して書けと言っているわけでもないですし、 今回追加・修正した部分で、書きやすいところからでいいから書いてみようという話なのですが、 頑なに文化がないから少しずつやっていきたいという人がいます。

私は、一体、これ以上、何を妥協したらいいのでしょうか・・・

あれでしょうか、達人になるまで戦場には出ないと言うやつなのでしょうか。気の長い話ですね。

なぜ怠慢と言っているのか

怠慢とは『なまけて、仕事や義務をおこたること』という意味だそうです(Google検索)。

未だにテストコードの重要性に関して説明が必要であれば、正直目眩を覚えるのですが、 少しでも気を紛らわせながら説明するのであれば、それは食事をして食器を洗わないようなものだとイメージして貰えるとわかりやすいのではないでしょうか。

食事を得るためには

自分で食事を作ろうと考えたのであれば、始めに献立を作成し、次に調理法の調査を行い、食材の調達に出かけ、調理を行うことで食事を用意することができるますが、食事が終わった後には食器の洗浄(料理後には調理器具の洗浄)も行わなければいけません。

もし洗浄の工程を行わなければ、洗われない調理器具や食器は流しに溜まり、 次の調理は難しくなり、次第に使える食器も不足して、最後には調理を諦めて、 インスタントや出来合いのものを食べるしかなくなってしまいます。

調理のテストと言う意味では『味見』という工程が正解なのでしょうが、あくまでやらなければいけない工程を省いたときのイメージと言う話でご了承ください。 『食事』を『リリース』と考えると、食事までの流れでは、工程的にテストの位置がおかしいですしね。

テストコードの重要性

テストコード作成は必要な手間なのです。

誰もテスト自体が無くて良いとは言わないように、テストの重要さは明らかなのですが、テストコードが重要視されていないのは、 テストという工程が他の工程よりも柔軟性が高いがために、テストコードを省略した方法でも一時的には目的が達成できてしまっているからでしょう。

だからテストコードが必要な手間なようには見えないのかもしれませんが、水で流すだけの食器を使い続けてたらどうなるのかとか、そういう話ではないかなと思います。料理の味が変わるだけならいいですが、知らずに繁殖した細菌を取り込んでから後悔するのでは遅いですから。

自信を持って自分の成果物を届けていくためにも、テストコードを書いていきましょう!

別の考え

それでも、どうしても、テストコードが書きたくないというのであれば、自分達で開発することは止めて、 誰かが作ったものを使うか、外注業者に頼むのがいいでしょう。
高めのお金を出して、味の好みはおまかせの、納得感がそこそこのものを食べるのもありなのかもしれません。

なぜなら私の家にはガスコンロすらないので。

Circle Checker作りで学んだこと(GroovyとかSpockでのモックの書き方とか)

技術的なこと

前回はCircle Checkerの紹介だったので、今回は作って学んだことを書こうと思います。

※Circle Checkerの紹介記事はこちらです。

実装の話

まずは、

「1時間で作る!」

とか言って始めたのに、プロジェクトの準備に30分かかっていた件ですが、 ローカルのgitリポジトリGitHubリポジトリを同期するのにコマンドを忘れ、時間がかかったためでした。

GitHub上のLICENSEファイルを使いたかったんです

リモートのGitリポジトリとローカルをマージ

以下が手順のコマンド。

git remote add origin git@github.com:seriwb/circle-checker.git
git fetch origin
git merge origin/master

ただ単に、マージの引数が足りていないだけでした。

便利なDTOの定義方法

Javaではプログラム間でやり取りするデータを扱うクラスをDTOと呼んだりしますが、 DTOは適切なコンストラクタやアクセッサメソッド、equals()、hashCode()の実装がほぼ必須になります。

Groovyではフィールド定義するだけでアクセッサメソッドは用意されますが、他は自前で用意する必要があります。

ですが、@EqualsAndHashCodeアノテーションを付けると、equals()とhashCode()の実装が行われ、 @TupleConstructorアノテーションを付けると、クラスフィールドの数に応じたパラメータを持つコンストラクタが利用できるようになります。

それらを利用してすると、以下のような記述だけでDTOクラスが実装できます。

import groovy.transform.EqualsAndHashCode
import groovy.transform.TupleConstructor

@EqualsAndHashCode
@TupleConstructor
class CircleInfo {

    String twitterName
    String twitterId
    String twitterUrl
}

Twitter APIのカーソリング方法

Twitter APIからカーソル付きの結果を得るときは最初に-1を投げること、とTwitterAPIドキュメントに記載があるので、 Twitter APIのラッパーであるTwitter4jの当該メソッドを利用する場合も、-1相当の値(long型なので-1L)を設定して呼び出す必要があります。

Circle Checkerで該当する実装箇所は、以下などです。

https://github.com/seriwb/circle-checker/blob/master/src/main/groovy/box/white/cc/CircleChecker.groovy#L156

クロージャ内での正規表現チェック

クロージャに渡った値をそのままクロージャ内の正規表現の一部に利用する場合は、 ダブルクォートで囲まれた文字列のときと同様に、$を先頭につけるだけで使えました。

def checkStr = "ああああiiiiうううう"

def filterList = ["aaaa", "iiii", "uuuu"]
filterList.each {
    if (checkStr =~ /.*$it.*/) {
        println(it)
        return
    }
}

※ each内でのreturnはeachを抜けるだけで、後続処理は継続されます

AccessTokenのクリアの仕方

AccessTokenオブジェクトのtokenとtokenSecretをnullに設定したもAccessTokenの設定がクリアできるわけではなく、以下の例外は発生します。

java.lang.IllegalStateException: Access token already available.

Twitter4jでAccessTokenをクリアするには、Twitter#setOAuthAccessTokenでnullをセットするのが正しい方法になります。

落ち穂

私がGroovyなのに型をわざわざ宣言しているのは、見返したときに何に利用しようとしていたかを確認するためと、IDEの補完を有効にするためです。

最初はdefで宣言してざっくりコードを書いて、後々型宣言に修正するという書き方を良くしています。

テスト(Spock)の話

過去にJUnitの記事を書いたように、業務ではJUnitでテストを書くことが多かったのですが、

個人的にJava/GroovyのテストフレームワークSpockに期待を寄せています。

洋書のJava Testing with Spock買って読んだりたりしていたわりには、 今まで実用レベルのテストをSpockで書いていなかったので、今回はMock/Spyの両機能を使ったテストコードを書いてみました。

Gradleの設定

GradleプロジェクトでSpockを利用するために、以下をbuild.gradleの依存関係に追加しますが、 Spockでテストを書くだけであれば、spock-coreだけで大丈夫です。 クラスをモックしたコードを書く場合は、cglibとobjenesisも必要になります。

testCompile "org.spockframework:spock-core:1.1-groovy-2.4"
testCompile group: 'cglib', name: 'cglib-nodep', version: '3.2.5'
testCompile group: 'org.objenesis', name: 'objenesis', version: '2.5.1'

モックとスタブ

モッククラスを作るのはとても簡単で、以下の2通りで宣言できます。
(モック対象のクラスをUserとして、モッククラスuserを宣言する場合)

  • def user = Mock(User)
  • User user = Mock()

モッククラスのメソッドにスタブさせる場合は、当該メソッドに対し、>>演算子を使って返却値を定義してあげるだけです。

user.getName() >> "ダミー名"

実際にモッククラスを使ってテストを実行している例は、以下になります。

   def "フィルタにある名前の結果のみが返却されることを確認"() {
     setup:
        List<String> filterList = ["ああ","uu"]

        def user = Mock(User)
        user.getName() >> name
        user.getScreenName() >> screenName
        user.getURL() >> url

        def file = '''
cc.target.list = "listname"
cc.html.dir = "./dir"
cc.tweet.maxcount = 100
cc.loop.waittime = 600
'''
        def config = new ConfigSlurper().parse(file)

        def testSuite = new CircleChecker(config)
        testSuite.filterList = filterList

     expect:
        testSuite.checkUserName(user) == expected

     where:
        name | screenName | url               | expected
        "ああああ" | "aaaa" | "http://aaaa.com" | new CircleInfo("ああああ", "aaaa", "http://aaaa.com")
        "いいいい" | "iiii" | "http://iiii.com" | new CircleInfo()
        "aaiiuu" | "uuuu" | null              | new CircleInfo("aaiiuu", "uuuu", "")
    }

setupでテスト実施までの処理を記載し、expectにはテストで確認したい比較条件のみを記載します。 また、複数のパラメータでテストを流すために、whereにそれぞれ動的に変更するパラメータを定義することができます。 記述順的に直感的ではないですが、whereで宣言しているパラメータはsetupやexpectでも使えます。

抽象メソッドの実装

後テストでよく必要になるのは、抽象メソッドを持つクラスのインスタンス化ではないでしょうか。

これはGroovyで実現できます。

例えば、Twitter4jのRateLimitStatusクラスに存在するgetRemaining、getLimit、getResetTimeInSeconds、getSecondsUntilResetを 適当な処理(呼ばれたら1を返す)で実装してインスタンス化する場合は、以下のようになります。

def status = [
 getRemaining: { -> 1 },
 getLimit: { -> 1 },
 getResetTimeInSeconds: { -> 1 },
 getSecondsUntilReset: { -> 1 },
] as RateLimitStatus

テスト対象クラスの一部をスタブ化

そして後はテスト対象クラスの一部をスタブ化できれば、大抵のテストは書けるようになるのかと思います。

Mockではインスタンス化したクラスのメソッドが全て実態のないものになっているので、Spyを使ってインスタンス化します。

以下は、PagableResponseListImpl#hasNext()を1度目はtrueを返し、それ以降はfalseを返すようにする実装です。

def users = Spy(PagableResponseListImpl, constructorArgs: [status, 1])
users.hasNext() >>> [true, false]

スタブの書き方は他にも色々あるので、実装時は以下などを参考にしてみてください。

また、MockとSpyの使い分けはこちらのサイトが参考になるかと思います。

@Unroll

Spockのパラメータテストの結果は、デフォルトではまとめて1テストとして出力されます。

これを各条件ごとに出力させるためには、@Unrollアノテーションをクラスかメソッドに対して付ける必要があります。
(基本は必要なメソッドに対して付けるのが良いかと思います)

@Unrollを付けていない場合、比較条件によってはエラー時にどの条件がダメだったのかがわからないので、 パラメータテストを行うメソッドには付与しておくことをお奨めします。

テストスイート

SpockにJUnitでいうところのテストスイートのような仕組みはないのですが、 Gradleでspock-coreを取り込むと、JUnitも依存ライブラリとして追加されます。

そこでJUnitでの書き方をGroovyにアレンジして、以下のようにすると、Spockでもテストスイートが実現できます。

import org.junit.runner.RunWith
import org.junit.runners.Suite
import org.junit.runners.Suite.SuiteClasses

@RunWith(Suite)
@Suite.SuiteClasses([
    CircleChecker_CheckUserNameSpec,
    CircleChecker_CheckFollowSpec,
])

class CircleCheckerSpecSuite {
}

今回はクラス名の末尾をSpecSuiteとしましたが、いつもならAllSpecにしていたかと思います。
末尾を統一させるか、末尾を分けて目立たせるかの違いですが、好きな方でいいと思います。

テストスイートのクラスを追加した場合は、テスト数が重複して計上されないように、当該クラスをtestタスクの対象外にしておきましょう。

build.gradleに以下の記述を追加してください(include/excludeは必要なものだけでOK)。

test {
    include '**/*Test*'
    include '**/*Spec*'
    exclude '**/*TestSuite*'
    exclude '**/*SpecSuite*'
    exclude '**/*AllTest*'
    exclude '**/*AllSpec*'
}

Spockでテストを書こう!

Spockのテストコードはどうでしたか?
過去にJUnitでモック処理を書いたり、パラメータテストを書いたことがある方であれば、魅力を感じてもらえるのではないかと思います。

とはいえ、ここまで単純化できているのは、テスト対象クラスのフィールドがGroovyらしくpublicになっているから、といいますか、 protectedのメソッドでも普通に直アクセスできるからなのですが。
この振る舞いは色々議論があるでしょうが、私としてはアクセス識別子はマーカー識別子くらいの認識なので、 プログラミングする上でアクセス識別子の考え方が頭に入っていれば、この動作でも問題ないと思っています。 ただ、プログラミング初学者などにはもしかしたら良くないのかもしれないなぁとも思ったりします。 ですが、Groovyを使う人でJavaを経験してこなかった人がどれだけいるのかと考えると、この件はただの杞憂ではないかなと。

極力単純に、かつ少ないコード量でのプログラミングするためのツールとして、Spockを使ってみるのはどうでしょうか?

【自作ツール紹介】 Twitterのユーザ名からイベント参加情報を抽出するツール「Circle Checker」

なぜ余裕を持って行わないのか・・・

原稿に追われていようといまいと、不精な性格が災いして、 イベント直前にならないとサークルチェックをしない私のような人に「余裕を持って行え」とか言っても無駄なので、 プログラマらしくプログラムで解決を図ってみました。

Circle Checker

イベントにサークル参加する方は、Twitterアカウントの名前に直前の参加イベントの情報を追加している事が多いので、 その情報を元にしたサークルチェックのツールを作りました。

使い方

まず、Javaをお使いのPCにインストールしてください。
READMEにはJava SEと記載していますが、Java REでも構いません。

今のインストーラなら環境変数への設定も自動で行われたような気がするので、 インストール後、WindowsならコマンドプロンプトMacならターミナルを起動し、 java -versionを入力し、実行した結果が正常に返ってくること(バージョンが1.8以上になっていること)が確認できたら大丈夫です。
(以降、コマンドプロントとターミナルのことをコンソールと表記します)

ツール準備

releasesからcircle-checker-X.X.X.zipのようなファイルを取得し、適当な場所に展開してください。
(コンソールを利用する関係上、日本語ディレクトリ配下ではない方が都合がいいです)

展開したディレクトリの中にある以下の設定ファイルを、条件に合うように修正してください。

  • config/config.txtcc.target.list = ""の"“の中にチェックするリスト名を入力
  • config/filter.txtにユーザ名に含まれる可能性のあるイベント名を列挙(デフォルトはコミティア用)

filter.txtは上から順番にパターンマッチを行うので、長い文字列は最初に持ってくるのをお奨めします。

また、上記ファイルの文字コードUTF-8である必要があります。
Windowsのメモ帳などでは文字化けする可能性があるので、sakura editorなど文字コード指定ができるエディタをご利用ください。

ここまでの準備ができたら、コンソールからcircle-checker-X.X.X.jarがある場所まで移動し、以下のコマンドを実行してください。 (X.X.Xはそのときのjarファイルのバージョン番号であると仮定します)

java -jar circle-checker-X.X.X.jar

リストのサイズによっては少し待ちますが、後は出力結果を元にサークルチェックするだけです!

ちなみに2013年のMacBook Proで1000くらいのユーザが登録されているリストを対象にして実行したところ、結果出力までに体感で4、5秒くらいかかりました。

似たようなツールあるけど何で作ったの?

ここまで説明しておいて何ですが、似たようなツールならWebで使えるものが検索するといくつがヒットします。

じゃあなぜ作ったのかというと、単純に自分の利用用途に合わなかったからです。
そして、その利用用途を叶えるため、このツールは以下の特徴を持っています。

Circle Checkerの特徴

  • リスト対象にチェックができる
  • チェックするリストを指定できる
  • フィルタの条件を自分で変えられる(マイナーなイベントでも対応できる)
  • 自分が動かしたいときに動かせる(アプリのWebサーバが止まっているという心配がない)

Webアプリではないので、スマホで処理したい!出先でチェックしたい!というような要望は叶えられませんが、 リストを使ってサークル管理していたり、自分でフィルタ条件をいじりたい人には使えるものになっているのではないかと思います。

また、他アプリによっては、サークルスペースも抜き出してくれるものがありますが、 サークル配置図にマッピングするための情報は「Twitter Name」に出ているので、今のところ追加する予定はありません。

ここのメンテでサークルチェックができなくなったら本末転倒なので。

雑記

前から作りたいなーと思っていたツールだったのですが、不精がすんなり作るわけもなく結局作らないでいたものの一つでした。

それをコミティアを当日に控えた深夜3時、

reinsのコード参考にしたら1時間でいけるんじゃ?!
そしたらTwitterとカタログ見るより早いな!!

と深夜テンションで始め、結局4時間くらいかけて終わらせ、

カタログ見た方が早かったのでは・・・

と思ったりもしてしまいましたが、それはさておき。

こんな理由で作ったものなので、要件を満たしたものがあっても既存のアプリは元々使わなかったんでしょうね。

後は、コードが公開されていないのをどこまで信じれるかというプログラマ故の面倒くさいところと、 単純にコーディングしたかったという理由もありますが。

実際に使ってみたところ、結果に出てくるTwitterアカウントへのリンクが便利でした。
iPhoneのメモに結果を貼っておいたら、URLをタップするとTwitterアカウントの表示ができたので、 電車の中でサークルチェックができました。

それで思いましたけど、お品書きが固定ツイートになっていると、すごく助かりましたね・・・

お陰でコミティアでは良い感じにお札が溶けたことを併記しておきます。

GradleプロジェクトのSpockのテストをCircleCIで実行してJaCoCoでカバレッジを取りつつCoverallsで表示する

ちょっと思い立ってJavaでコードを書いていて、 さらに思い立ってカバレッジのバッジをREADMEに付けようとしたら、 思いの外ハマったので「GradleプロジェクトのSpockのテストをCircleCIで実行してJaCoCoでカバレッジを取りつつCoverallsで表示する」ための手順を残しておきます。

やりたいこと

  • GradleプロジェクトのREADMEにCircleCICoverallsのバッジを表示したい
  • JavaコードのテストをSpockで書きたい
  • JavaコードのカバレッジJaCoCoで集計したい
  • GitHub/CIrcleCI/Coverallsの結果をSlackに通知する(ブログでは言及しない)

つまりこういう感じのことをしたいわけです。

f:id:seri_wb:20170320005901p:plain

ではやっていきます。


build.gradleにJaCoCoとCoverallsの設定を追加

Spockでテストを書くJavaのGradleプロジェクトは以下のコマンドで作成していました。

$ gradle init --type java-library --test-framework spock

このプロジェクトでCoverallsを使うため、以下のGradleプラグインを導入します。

build.gradleにpluginsとjacocoTestReportを追加すると使えるようになります。

plugins {
    id 'jacoco'
    id 'com.github.kt3k.coveralls' version '2.8.1'
}

apply plugin: 'java-library'
apply plugin: 'groovy'

省略

jacocoTestReport {
    reports {
        xml.enabled true // coveralls plugin depends on xml format report
        html.enabled true
        html.destination "${buildDir}/jacocoHtml"
    }
}

Coverallsで観るだけならhtml関連の設定は不要なのですが、ローカルでテストしたときにもカバレッジ結果を見たかったので入れてます。
The JaCoCo Plugin - Gradle User Guide Version 3.5

※実際に適用した場合はこのようになります。

CircleCIでテストする

CircleCIでテストを動作させるためには、まずCircleCIにアカウントを作成して、メニューのPROJECTSからテストしたいプロジェクトをBuild projectにします。

その前後でテスト対象のプロジェクトにcircle.ymlファイルを作成して、GitHubにプッシュします。
例えば、Javaのプロジェクトをテストするcircle.ymlの内容は、以下のようになります。

machine:
  timezone: Asia/Tokyo
  java:
    version: oraclejdk8

test: 
  override:
    - ./gradlew test jacocoTestReport coveralls

ここが失敗ポイントだった

作業していたときに上手くいかなかった原因を話してしまうと、ここで実行しているgradlewのコマンドを coveralls-gradle-pluginのREADMEに書いてある通り./gradlew jacocoTestReport coverallsとしていたためでした。

というのも、testを書いていないとtestタスクがSKIPされ、結果カバレッジも取得されないという状態になっていたようです。

つまり以下のような一文をbuild.gradleに足して、jacocoTestReportタスクがtestタスクに依存することを示しておけば、 testを書き忘れてもちゃんとカバレッジが取れるようになります。

jacocoTestReport.dependsOn test

CircleCIとCoverallsを連携させる

CircleCIでカバレッジが取れるようになったら、次はCoverallsの設定をします。

CircleCIと同様に、Coverallsもアカウントを作成し、メニューのADD REPOから対象のプロジェクトをONにします。
その後、SETTINGSの画面に表示されるREPO TOKENの値をメモします。

次はCircleCIの方に移り、プロジェクトの設定メニューからBUILD SETTINGSのEnvironment Variablesを選択し、以下の変数を追加します。

  • COVERALLS_REPO_TOKEN : CoverallsのREPO TOKENの値

この設定まで終わった状態で、CircleCIでビルドを行うと、Coverallsの方にカバレッジの結果が表示されるようになります。

READMEにバッジを表示

最後にREADME.mdにCircleCIとCoverallsのバッジを表示させます。

CircleCI、Coveralls共に、バッジをREADMEに表示するためのテキストを生成してくれています。
それぞれコピーして、対象プロジェクトのREADME.mdに貼り付ければバッジの表示は完了です。

CircleCI

f:id:seri_wb:20170320035858p:plain

CircleCIでのビルド状態を示すバッチは、以下の2通りで表示することができます。

CircleCIhttps://circleci.com/gh/seriwb/utsusemi.svg?style=svg

CircleCIhttps://circleci.com/gh/seriwb/utsusemi.svg?style=shiled

Coveralls

f:id:seri_wb:20170320040009p:plain

感想:結構大変だった

READMEに表示されているバッジの仕組みについて、気にはなっていましたが、今までスルーしていたので、その知識が補えて良かったです。 今回は結構苦戦してしまいましたが、わかっていれば10分もあれば終わりそうでした。(今回は4時間ぐらいかけたんですけど。。)

ともあれ、これでいつでもどこからでもテストとカバレッジの状態が見えるようになったので、 後は頑張ってコードを書いていこうと思います。