JGiven をユーザーガイド見ながら試す(Java, JUnit)
JGiven を使おうとしたきっかけ
Java のプロジェクトに関しては、普段 JUnit を使用してテストを書いていますが、以下のような問題に悩まされています。
1. 既存テストの要点を掴むのが難しいことがある
他人が書いたテストコードはもちろんのこと、自分で書いたテストですら数ヶ月も経つと、テストの要点(目的)などが分かり難くなってしまっていることがあります。
テスト内でテストデータを作ったり、条件を作ったりしていると、それをトレースするのが大変になったりします。
2. テストのレポートがわかりにくい
JUnit でテストを実行した結果、例えその結果が全てパス(成功)だったとしても、どういった仕様を満たしているのかわかりにくい場合があります。
特にテストケースが数十、百以上になってきたりすると、どのようなテストケースがあるのか把握するのが難しくなります。
3. 開発者(プログラマー)しか読めない
上記と類似することですが、テストの仕様を理解するのに、プログラム(ソースコード)を読まなければならない場合があるため、開発者以外が仕様を理解することが難しくなります。
場合によっては、開発者以外のために、別途ドキュメントを用意する必要があります。
結局のところ、以下の2点が満足できれば、これらの問題を解消する手助けになりそうです。
1. 読みやすいテストが書けること
2. 分かりやすいレポートが出力されること
これらが、JGiven フレームワークに期待しているポイントになります。
ユーザーガイド
それでは、ユーザーガイドに沿って、インストールから最初の実行までやってみます。
JGiven User Guide
インストール
JUnit と maven を使用しているので、以下を pom に指定してあげます。
<dependency>
<groupId>com.tngtech.jgiven</groupId>
<artifactId>jgiven-junit</artifactId>
<version>0.17.0</version>
<scope>test</scope>
</dependency>
JUnit テストクラスを作る
何はともあれ、テストクラスがなければ話が始まらないので作ります。
JUnit の場合は、ScenarioTest を継承する必要があるようです。
更に ScenarioTest は3種類のパラメータを要求しており、それぞれ Given-When-Then と呼ばれます。
import com.tngtech.jgiven.junit.ScenarioTest; public class MyShinyJGivenTest extends ScenarioTest<GivenSomeState, WhenSomeAction, ThenSomeOutcome> { }
日本語で言うと、以下のようなステージ構成になるようです。
Given : 何かの状態のとき
When : 何かをしたら
Then : 何かの結果になる
これを上手く表現することが出来れば、そのまま仕様のように読み取れるかもしれません。
Given, When, Then クラスを作る
それぞれのステージを構成するためのクラスを生成します。
Given
import com.tngtech.jgiven.Stage; public class GivenSomeState extends Stage<GivenSomeState> { public GivenSomeState some_state() { return self(); } }
When
import com.tngtech.jgiven.Stage; public class WhenSomeAction extends Stage<WhenSomeAction> { public WhenSomeAction some_action() { return self(); } }
Then
public class ThenSomeOutcome extends Stage<ThenSomeOutcome> { public ThenSomeOutcome some_outcome() { return self(); } }
Stage クラスの継承を必須としているわけではないそうですが、and() や self() などの有用なメソッドが提供されているため、推奨されているようです。
※何が有用なのか現時点では不明ですが
テストシナリオを書く
それではいよいよテストシナリオを書いていきます。
最初に作ったテストクラスに追記しています。
public class MyShinyJGivenTest extends ScenarioTest<GivenSomeState, WhenSomeAction, ThenSomeOutcome> { @Test public void something_should_happen() { given().some_state(); when().some_action(); then().some_outcome(); } }
ScenarioTest を継承したことにより、given(), when(), then() がそれぞれ指定したクラスのインスタンスに紐づいているようです。
なので、インスタンス.メソッド名 という形でメソッドの呼び出しが可能となっています。
実行してみる
実行してみると、以下のような結果が出力されました。
Given, When, Then は固定で、some xxx は、メソッド名(some_state など)から来ているようです。
Test Class: test.java.practice.bdd.jgiven.MyShinyJGivenTest Something should happen Given some state When some action Then some outcome
なので、例えば日本語でメソッド名を作成してみた場合、こんな感じの出力が期待できます。
どんなテストをやっているのか分かりやすいですね。
Given サーバーが止まっている時に When プログラムを実行すると Then エラーになる
もし ScenarioTest を継承することができない場合
すでに他のクラスを継承しているなどで、ScenarioTest を継承することができない場合は、JGiven の JUnit rule を使用すれば良いとのこと。
具体的には、JGivenClassRule と JGivenMethodRule を定義すること。
Given-When-Then それぞれのクラスを @ScenarioStage アノテーションを用いて注入してあげること。
public class UsingRulesTest { @ClassRule public static final JGivenClassRule writerRule = new JGivenClassRule(); @Rule public final JGivenMethodRule scenarioRule = new JGivenMethodRule(); @ScenarioStage GivenSomeState someState; @ScenarioStage WhenSomeAction someAction; @ScenarioStage ThenSomeOutcome someOutcome; @Test public void something_should_happen() { someState.given().サーバーが止まっている時に(); someAction.when().プログラムを実行すると(); someOutcome.then().エラーになる(); } }
ScenarioTest のソースを見てみると、似たようなことをしているので、継承する代わりに自分たちで定義してあげるような格好になっていると思われます。
public class ScenarioTest<GIVEN, WHEN, THEN> extends ScenarioTestBase<GIVEN, WHEN, THEN> { @ClassRule public static final JGivenClassRule writerRule = new JGivenClassRule(); @Rule public final JGivenMethodRule scenarioRule = new JGivenMethodRule( createScenario() ); @Override public Scenario<GIVEN, WHEN, THEN> getScenario() { return (Scenario<GIVEN, WHEN, THEN>) scenarioRule.getScenario(); } }
まとめ
以上、ユーザーガイドに沿って試行してみましたが、当初期待していた2点
> 1. 読みやすいテストが書けること
> 2. 分かりやすいレポートが出力されること
について、JGiven が手助けになる可能性を感じることができました。
もちろん現時点では、テストケースも少なくシンプルなので分かりやすいですが、実際にシナリオが増えてきた場合に、上手くシンプルな状態を保ったまま管理が出来るかは分かりません。
とは言え、テスト結果が以下のように表示されるだけでも強力です。
Given サーバーが止まっている時に When プログラムを実行すると Then エラーになる
たとえば、以下のように Given だけ差し替えることも容易に出来ると思われます。
Given ネットワークが止まっている時に When プログラムを実行すると Then エラーになる
レポート出力に関しては、機能が充実しているようなので、機会があれば試行してみたいと思います。
terminator(端末)が起動しなくなった except (KeyError,ValueError)
Ubuntu14.04 上でいつの間にか terminator が起動しなくなりました。
デスクトップ上のランチャーを叩いても、何の応答もありません。
仕方なく、他の端末(XTerm)を起動し、/usr/bin/terminator を直接実行してみたところ、以下のエラーが表示。
File "/usr/bin/terminator", line 103 except (KeyError,ValueError), ex: SyntaxError: invalid syntax
SyntaxError ? なぜ、terminator のソースをさわってもいないのに突然そのようなエラーが出るのか。
答えは、python 3 をデフォルトに設定していたためでした。
※確かに、この問題が発生する直前に変更していました
/usr/bin/terminator の1行目に、#! /usr/bin/python と指定されています。
そのため、python 3 で terminator を起動するようになったと思われますが、それが互換性がなくて SyntaxError になってしまったと思われます。
回避策として、1行目を #! /usr/bin/python2 として、python2を適用することを明示すれば、問題なく起動するようになりました。
この問題は初めて遭遇しましたが、前々からよくありそうですし、報告されています。
bugs.launchpad.net
タイガースとジャイアンツが含まれるツイート件数をカウントしてみた
データ取得期間
2018/11/11 - 11/13 内の数時間
データ取得方法
集計ログ
取得した総ツイート件数
scala> val rawData = sc.textFile("タイガース_ジャイアンツ.txt") rawData: org.apache.spark.rdd.RDD[String] = タイガース_ジャイアンツ.txt MapPartitionsRDD[19] at textFile at <console>:27 scala> rawData.count res24: Long = 1650
ツイートにタイガースが含まれる件数
scala> def isTigers(line: String): Boolean = line.contains("タイガース") isTigers: (line: String)Boolean scala> rawData.filter(isTigers).count res25: Long = 551
ツイートにジャイアンツが含まれる件数
scala> def isGiants(line: String): Boolean = line.contains("ジャイアンツ") isGiants: (line: String)Boolean scala> rawData.filter(isGiants).count res26: Long = 1196
1つのツイートにタイガースとジャイアンツの両方が含まれているものもある。
しかし、ジャイアンツが含まれているツイート数がタイガースよりも、およそ2倍という結果に。
ツイートに矢野監督が含まれる件数
scala> def isYano(line: String) = line.contains("矢野監督") isYano: (line: String)Boolean scala> rawData.filter(isYano).count res30: Long = 5
ツイートに原監督が含まれる件数
scala> def isHara(line: String) = line.contains("原監督") isHara: (line: String)Boolean scala> rawData.filter(isHara).count res31: Long = 11
もちろん、これだけで世間の注目度を示しているわけではないし、集計期間や集計方法にも問題がたくさんあると思いますが、それにしても思っていた以上に数値に差が出る結果となりました。
もう少しサンプルデータを増やした上で、別の角度からも集計をしてみたい。
grep -E が正規表現対応オプションであると勘違いしていた
grep って、global regular expression print の略なので、それ自体が正規表現で一致したものを抽出するコマンドなんですね。
正規表現を使いたい時に、-E オプションを付けるものだと勘違いしていました。
いま、以下のように2行が書かれたファイル(test.txt)があるとします。
$ cat > test.txt 1024 hatena
$ grep ^1 test.txt 1024
はい。特に -E オプションを付けなくても正規表現でマッチしてくれました。
ところが。
$ grep '^(1|h)' test.txt
これは何も返してくれません。
そこで、-E オプションを付けてみると。
$ grep -E '^(1|h)' test.txt 1024 hatena
期待する結果を返してくれました。
E オプションをつけることで、拡張正規表現と呼ばれる式が使用出来るようになります。
egrep でもOKです。
$ egrep '^(1|h)' test.txt 1024 hatena
拡張正規表現を使える = 正規表現を使えると間違って覚えてしまっていたようです。
何気に使用しているオプションについて、意味を正しく理解していないのは宜しくないですね。
開発者が知っておくべき Couchbase についての 10項目
はじめに
公式ブログによって、こうやってまとめておいてくれると、読みやすいし初学者にとって助かります。
blog.couchbase.com
第10位
Document access in Couchbase is strongly consistent, query access is eventually consistent
Couchbase のデータには強い一貫性があると主張されています。
key / value アクセスなので、そこは保証されやすいところなのかと思います。
View (恐らくインデックス)についても、最終的には一貫性が保たれるということですが、逆に言うと一貫性がない瞬間(インデックスが生成されるまで)もあるということですね。
第9位
Writes are asynchronous by default but can be controlled
書き込み処理は基本的に非同期で行われるようです。
レプリカの生成と、データの永続化(メモリからディスクへの書き込み)はバックグラウンドで行われ、クライアント側はその結果の通知を受け取れるようです。
クライアント側で先に通知を受け取るか、通知よりも先に非同期にレプリカを作成させるかなどはクライアント側で選べるようです。
第8位
Couchbase has atomic operations for counting and appending
カウント処理と追加処理において、不可分操作をサポートしているようです。
例にあるように、incr は書き込み処理と結果を返す処理を行います。
要は追加に失敗したのに、追加されたものとしてカウントされてしまったりすることは無いということかと思います。
cb.set(“mykey”, 1) x = cb.incr(“mykey”) puts x #=> 2
第7位
Start with everything in one bucket
Bucket というものがデータベースのようなものであり、RDBMSでいうところテーブルというわけではない。
そのため、RDBMSからそのままデータを移すとしたら、複数のテーブルが1つのBucket に全て投入されることになります。
ただし、"typte" という属性が恐らくデータ毎に設定が可能であるため、それを設定することで同じBucket内にあるデータであっても差別化ができるようです。
とにかく1つのBucketでスタートすることが推奨されているようですね。
第6位
Try to use 5 or less buckets in Couchbase. Never more than 10.
データは固定のスキーマを持つわけではないので、色々なスキーマのデータを同じBucketに入れることが可能。
ソフトウェアとして限界値は定められていないものの、10 Bucketにもなると、CPUやDiskIOに問題が発生することが確認されているようです。
この辺りは Couchbase のバージョンアップによって、改善される可能性はあるかもしれませんが、出来る限り少ない数の Bucket で運用する方が良さそうですね。
まぁ、もしどうしても Bucket をたくさん作って管理を分けたいのであれば、別のクラスタに切り離してしまった方が良いのかもしれません。
第5位
Use CAS over GetL almost always
CAS は "Check and Set"の略のようです。
KVSにおけるトランザクション処理を行うために必要な操作のようです。
要は楽観的ロックと悲観的ロックの話をしていますが、基本的に楽観的ロックを使用するべきだということでしょうか。
Couchbaseにおけるロック機能というものを知らないので、これ以上は理解が出来ていません。
第4位
Use multi-get operations
Couchbase には、キーのリストから複数のレコードを同時に?検索できるようです。
個々のキーを一つずつ検索するよりも、高パフォーマンスが期待できるようです。
第3位
Keep your client libraries up-to-date
これは、Couchbase に特化した話でもないと思いますが、使用するライブラリは最新のものが良いよということですね。
バージョンアップが頻繁に行われている製品であれば、何であっても同じことが言えると思います。
第2位
Model your data using JSON documents
格納するデータはJSON形式で。
CouchbaseはJSON形式や、バイナリ形式のドキュメントをサポートしていますが、まずはJSON形式で試すことを推奨しているようです。
JSON形式で保存しておくと、インデックスが作成できたり、特殊な?クエリを投げられるなどメリットがありそうです。
第1位
Use indexes effectively
インデックスを効果的に使うこと。
出来る限り、プライマリキーでのアクセスを行うこと。キーとメタデータをメモリ上に持っているので、アクセスは速いはず。
セカンダリ・インデックスでのアクセスは、パフォーマンスを必要としない分析用などで使用するべきとのこと。
この時点で、セカンダリ・インデックスのパフォーマンスは、それ程期待しない方が良いのかもしれません。
4つの design documents と、1つの design documents あたり、view は10個以下に抑えること。
それでも多いようなきもしますが。
インデックスデータに対して、何の "reduce" 処理を行う必要がないのであれば、value に "null" を設定しておきべきとのこと。
おわりに
これを読んだだけでは表面的な部分しか分かりませんが、使用していく内に「あれはそういう意味だったのか」のように、それぞれの意味をより深く知ることが出来るのかもしれません。
”Use indexes effectively” というのが一見当たり前のように見えて、Couchbase を使う上で実はとても大切なことのような気がします。
新しく使用する技術に対して、「取り合えず使ってみる」アプローチと、「概念をまず理解する」アプローチどちらが良いかは人それぞれかと思います。
自分の場合、取り合えず使ってみて、分からないところが出てきたら調べることが多いように思います。
ただ、エラーを回避することに精一杯になってしまって、結局その製品のことをあまり理解できていない事に後で気づくので、手を少し止めてインプットする時間も意識して作れるといいなと思います。
リファクタリングと追加実装はコミットを分けてほしい
主にソースコードをレビューする立場である場合の視点になります。
チームで開発しているとき、リファクタリングと追加実装を同時にレビュー提出されることがあります。
ただ、レビュー依頼のコメントには、リファクタリングのことは触れられていないので、レビューする側としては、全てが追加機能の実装に伴う変更点であると最初は解釈します。
しかしながら、レビューを進めていくとどうも追加実装の要件とは関係の無いところまで編集されている様子。
よくよく確認してみると、気になった箇所のリファクタリングをついでにやったとのこと。
気になったソースコードをそのまま放置せずにリファクタリングする試みはとても良いことだと思うのですが、個人的には出来ればコミットは分けておきたいところです。
コミットを分けておくことで、リファクタリング箇所と追加実装箇所を明確に分けて見ることができるため、レビューもしやすくなります。
あと、リファクタリングを行った段階で単体テストを通しておけば、そのリファクタリングによって不具合が作られなかったことの確認も容易にできます。
以前あったのが、リファクタリングと追加実装を同時に行った後、単体テストが通らなくなり、ずっと追加実装したコードを疑っていたが、実はリファクタリングした箇所が不具合を作ってしまっていました。
チームで開発作業していると、他人が実装したプログラムを引き継いだり、逆に自分が実装したプログラムを引き継いてもらったりなど日常茶飯事なので、少しずつ小さなステップで確認しながら実装していかないと、簡単に不具合が混入されてしまいます。
1.コード規約が定まっていない(個人的にはあまり細かく縛られたくないですが)
2.ソースコード内のコメントが不親切
3.詳細設計書がない
こういった場合、良くも悪くもプログラマーの個性がソースコードに表れるため、理解が難しくなる場合があります。
※数ヵ月後には自分自身が書いたものですら、理解出来なくなってしまうことも
切羽詰っているプロジェクトほど、こういったステップを踏む余裕はないかもしれませんが、後戻りを防ぐためにも確実に進めるようにしていきたいものです。
Java 無名(匿名)クラスを意識して使ってみる
Javaでプログラムの基本は、クラスのインスタンスを生成し、そのインスタンスから目的のメソッドを呼ぶところにあると思いますが、結果的に一度しかインスタンス化されないクラス(オブジェクト)があったり、ある特定のクラスからしか必要とされないインスタンスがあったりします。
自分はプログラミングする際、1つのクラスに多くの役割を持たせたくない(役割分担させる)ため、クラスを細かく定義することを基本にしていますが、中には一度しかインスタンスが生成されない、メソッドが一つしかないクラスを作ってしまうことがあります。
特に大きな問題になる訳ではないですが、単純な処理しか行っていないのであれば、そのメソッドを定義する無理にクラスを定義せずに、その処理を必要としているクラス内に含めてしまっても良いかもしれません。
無名クラスという仕組みを使えば、インターフェースを実装したクラス(あるいはあるクラスを継承したサブクラス)の宣言を省略することが出来るので、その辺りを上手く使えないかと試みたのですが(今までは意識して使ったことなし)。
クラスを定義しインスタンスを明示的に生成する場合
interface Dao { String getData(); } class MySqlDao implements Dao { @Override public String getData() { return "MySql data from MySqlDao.class"; } }
MySqlDao mySqlDao = new MySqlDao();
System.out.println(mySqlDao.getData());
Dao インターフェースを実装した MySqlDao クラスを定義しています。
呼び出し側では、そのインスタンスを生成しています。
無名クラスを使う場合
interface Dao {
String getData();
}
Dao mySqlDao2 = new Dao() { @Override public String getData() { return "MySql data from anonymous class"; } }; System.out.println(mySqlDao2.getData());
まるで Dao のインスタンスを生成しているように見えますが、ここでは Dao インターフェースを実装した無名クラスのインスタンス(mySqlDao2 )が生成されていることになります。
※そもそもインターフェースのインスタンスを生成することはできない
結局、クラスの宣言と利用を同時に行っているだけではあるのですが、これによって MySqlDao.class の定義を省略することが可能になりました。
管理するクラスの数が減るという面でメリットがあると思います。
一方で”無名”のクラスになってしまうので、そのクラス名からどのような役割を持っているのかを推測することが出来なくなります。
例が悪かったですが、Dao などは外部から呼び出される前提のクラスになると思いますので、広く使われるようなクラスは無名クラスにするべきではなさそうです。
本当にそこでしか使わない(使い捨てに出来る)クラス限定で適用するものかと思います。
さらに無名クラスになれるのは、インターフェースを実装したクラスあるいはサブクラスに限定されるので、今のところ個人的には活躍の場はそれ程ない見込みです。