思うにAnnotationは
MVCで言うならVのようなソフトウェアのプレゼンテーションとかUIとかを担う層とも言える。
モデルとビューを分離するのはビューへの変更は頻繁に起こることから来ていて、そしてそれはユーザの選好に大きく依存するからなわけだ。
するとアノテーションの体系はユーザの選好に基づいて組み立てられてしかるべき(と言えそう)なのだが、APIライブラリのインタフェースとしてアノテーションを使う以上、そんなことはできないよね。
アノテーションもクラスなので、モデルに現れるクラス名との衝突を回避するために自然な名前を使いにくいというのも困る。(無論パッケージが違うので全くできないということではないのだが、一つのファイル内でimport文によってFQCNを省略できる同名のクラスはひとつだけだ。)
世間の人々はどうして(どう考えて)いるのだろう。
JCUnitによる有限状態機械のテスト - 有限状態機械テストの方法(その3)
考えてみれば、ソフトウェアシステムというのは有限状態機械であり、ソフトウェアシステムの仕様を決めるとはその有限状態機械の仕様を決めることに他ならない。
と、いうことは有限状態機械の仕様からテストを自動的に生成して、それを自動的に実施できるなら、人間はソフトウェアシステム自体の設計と開発に集中してあとのことは計算機にやらせればよい、ということにならないだろうか?
なったらいいなあ。
今回のFSMサポートの目的はこの辺だ。
では、有限状態機械の仕様をどのようにJCUnitに伝えればよいだろうか?
ということでいよいよ、JCUnit上で如何にして有限状態機械を定義するかの解説に移る。
jcunit/FlyingSpaghettiMonsterTest.java at develop · dakusui/jcunit · GitHub
public static enum Spec implements FSMSpec<FlyingSpaghettiMonster> { @StateSpec I { @Override public boolean check(FlyingSpaghettiMonster flyingSpaghettiMonster) { return flyingSpaghettiMonster.isReady(); } @Override public Expectation<FlyingSpaghettiMonster> cook(FSM<FlyingSpaghettiMonster> fsm, String dish, String sauce) { return FSMUtils.valid(fsm, COOKED, CoreMatchers.startsWith("Cooking")); } }, @StateSpec COOKED { @Override public boolean check(FlyingSpaghettiMonster flyingSpaghettiMonster) { return flyingSpaghettiMonster.isReady(); } @Override public Expectation<FlyingSpaghettiMonster> eat(FSM<FlyingSpaghettiMonster> fsm) { return FSMUtils.valid(fsm, COOKED, CoreMatchers.containsString("yummy")); } @Override public Expectation<FlyingSpaghettiMonster> cook(FSM<FlyingSpaghettiMonster> fsm, String dish, String sauce) { return FSMUtils.valid(fsm, COOKED, CoreMatchers.startsWith("Cooking")); } },; @ActionSpec public Expectation<FlyingSpaghettiMonster> cook(FSM<FlyingSpaghettiMonster> fsm, String pasta, String sauce) { return FSMUtils.invalid(); } @ParametersSpec public static final Object[][] cook = new Object[][] { { "spaghetti", "spaghettini" }, { "peperoncino", "carbonara", "meat sauce" }, }; @ActionSpec public Expectation<FlyingSpaghettiMonster> eat(FSM<FlyingSpaghettiMonster> fsm) { return FSMUtils.invalid(); } }
途中をすっとばすならば、上記の内部クラスSpecがJCUnitに渡す有限状態機械の仕様そのものだ。
JCUnitはこのSpecの定義内容を解析し、テストを生成し、実行する。
Specは別に内部クラスでなくてもよいし、enumでなくてもよいが以下の条件を満たす必要がある。
- SUTは任意のクラス。
- FSMSpec
インタフェースを実装する必要がある。 - 状態を定義するには、フィールドを定義し、これにStateSpecアノテーションを付す。これらのフィールドは以下の条件を満たす必要がある。
- 状態遷移関数を定義するにはメソッドを定義し、これにActionSpecアノテーションを付す。これにより、SUTにある同名のメソッドが必要なタイミングで自動的に呼ばれるようになる。上掲の例で言うと、"cook"メソッドと"eat"メソッド。これらのメソッドは以下の条件を満たす必要がある。
- 上述の状態遷移関数に渡す引数がある時には、同名のフィールドを定義し、ParamsSpecアノテーションを付す。上掲の例で言うと、"cook"フィールド。このフィールドは以下の条件を満たす必要がある。
うーん。
頑張ってシンプルにしたんだけど、書き下してみると、けっこうややこしいなあ。
なんにしても、有限状態機械を定義するのには必要な情報だし、なれれば難しくはないと思う(多分)
。。続きます。
JCUnitによる有限状態機械のテスト - 有限状態機械テストの方法(その2)
さて昨日に引き続き、JCUnitによる有限状態機械のテストの書き方について。
テストメソッドの宣言
これはどうということもなくて
public FlyingSpaghettiMonster sut = new FlyingSpaghettiMonster(); @Test public void test() throws Throwable { FSMUtils.performScenarioSequence(this.main, this.sut, ScenarioSequence.SIMPLE_REPORTER); }
こう実装してしまえば終わり。
mainは昨日のエントリで言及したmainフィールドで、テスト内容ほぼそのものといっていいScenarioSequenceのオブジェクトが格納されている。
FSMのテストは、「ある状態で(Given)どんなことをやったら(When)どうなるべきか(Then)」にほかならない。(とJCUnitでは解釈している)
SUTが前回までにおこなった操作によっても挙動が変わり得ることを考えると、このGiven、When、Thenを何個か連ねた場合もテストするべきだろう。
このGiven, When, ThenをまとめたものがScenarioで、Scenarioを連ねたものがScenarioSequenceだ。
このScenarioSequenceをSUT (Software under test - テスト対象ソフトウェア)に対して実行する。
これがテストだ。
this.sutには、SUTが格納されている。
だから、上の一行でテストが実行できる。
ちなみに三番目の引数は、テストの実行状況をレポートするためのオブジェクト。
ひとまずここでは「クリシェ」ということでひとまず説明を割愛する。
そしてやや分かりにくいかもしれないが、大事なのがこれ。
@Before public void before() throws Throwable { FSMUtils.performScenarioSequence(this.setUp, this.sut, ScenarioSequence.SILENT_REPORTER); }
前日のエントリではテストの「準備」をする、と書いた。
なんで準備が必要かというとこういうことだ。
ある状態機械が3つの状態を遷移するとしよう。
S0 - (a0) -> S1 - (a1) -> S2
最初の状態がS0、それにイベントa0が起こり、S1、さらにそこにa2がおこりS2に遷移する。
このとき、S0やa1に対応する状態やイベントを様々に変えてテストしたい。この「様々に変える」のにペアワイズのテクニックを使っているのが、今回実装したJCUnitによる有限状態機械テストサポートの中身だ。
もちろん、状態機械の設計によっては、存在し得ないようなパターンがたくさん存在する。たとえば、「使用済」状態の「お皿」オブジェクトを「未使用」状態にもどすことはできない、とか、buildメソッドが呼ばれる前にsetFactoryメソッドが呼ばれていないといけない、とか。
これは「制約」と呼ばれるもので表現すればよい。JCUnitのConstraintManagerにあたる。
ところが、状態機械がとり得る状態やそれに与えるイベントを因子や水準とみなすとしてもそれらの間にどのような制約があるかはかなり複雑だ。
複雑である一方、状態機械の仕様がひとたび与えられれば、機械的に決まる。こういものは計算機にやらせるべきことであって人間が手でやるべきことではない。
人間は状態機械の仕様を理解したり設計したりすることに集中するべきで、そこからテストを作ったり実行したりというのは計算機にやらせたいのだ。
閑話休題。
上述の状態機械が3つの状態を遷移する場合の話に戻る。
S0が様々な状態をとる。S1も様々な状態をとる。以下、同様。でも、S0に指定された状態が、テスト対象となるソフトウェアの初期状態と違う場合はどうすればよいのだろう?
一つの考え方はS0は、必ず状態機械の初期状態だという制約を設けてしまうことだ。実は当初、そういう方法で実装してみたのだが、うまく行かなかったのだ。うまく行かなかった理由の詳細はいずれ別の機会に述べることもあると思うが、今は先を急ぐ。
結局、JCUnitのFSMサポートでは、S0は状態機械がとり得る状態(水準)が全部割り当てられることにし、そしてS0にたどり着くための方法は別の問題として別に解くことにした。
ではS0にたどり着くための手順はどうすれば生成できるのか?
言うまでもない。与えられたFSMの仕様から、S0に割り当てられた状態にたどりつく経路を探索すればよいのだ。
そうして得られた経路というか手順をたどれば、SUTを所定の状態にもっていくことができるはずだ。
そしてそれが、
@FactorField( levelsProvider = FSMLevelsProvider.class, providerParams = { @Param("flyingSpaghettiMonster"), @Param("setUp") }) public ScenarioSequence<FlyingSpaghettiMonster> setUp;
このフィールドに格納されており、以下のコードで実行できる。
@Before public void before() throws Throwable { FSMUtils.performScenarioSequence(this.setUp, this.sut, ScenarioSequence.SILENT_REPORTER); }
とまあこういうわけだ。
JCUnitによる有限状態機械のテスト - 有限状態機械テストの方法(その1)
はじめに
かれこれ三カ月くらい頑張って、有限状態機械(以下、FSM)をJCUnitを使ってテストする機能を作ってみた。
今回はひとまず0.4.20としてリリースした。MavenのCoordinateは以下のとおり。
<dependency> <groupId>com.github.dakusui</groupId> <artifactId>jcunit</artifactId> <version>0.4.20</version> </dependency>
テストは十分ではないので、バグはまだあると思う。
この追加はかなり大きなものだったし、実用性という点でもひとつの区切りになるものだと思う。
デバッグが終わったら、0.5.0としてリリースしなおしたい。
使い方を解説する記事を書こうと思ったのだが、かなりの分量になりそうなので、何回かに分けてここのブログに掲載したい。
大幅な変更は難しいかもしれないが、多少の機能やインタフェースの変更は可能なので、コメントをいただければ幸いである。
また、記事中に分かりにくい点などあれば、これもコメントをいただきたい。
まず、サンプルはこちら。
テストケースの内容に応じてテストメソッドの実行を制御する方法。
JCUnitのように、テストケースの集合を自動で生成する仕組みを作ったのに、確認すべき内容や実行すべきシナリオがテストケースの内容によって違う場合、それらの確認内容を振り分けるロジックをユーザにif文でちまちま書かせるのはいささかだせえ。
そこで、テストケース(各因子がとる水準)の内容に応じてあるテストメソッドが実行されるべきかどうかを制御できる仕組みを作った。
なおこの機能は0.4.12で利用可能である。
実例と記法。
さっそくだが、例を掲げる。
public boolean aIsNonZero() { return a != 0; } public boolean discriminantIsNonNegative() { return b * b - 4 * c * a >= 0; } @Test @When("aIsNonZero&&discriminantIsNonNegative") public void solveEquation() { // 1. solve it. // 2. make sure the solutions are precise. }
WhenはAnnotationとして、このテストメソッドが実行されるべき条件を論理式として記述する。
以下、使い方を述べる。
単純な真偽の状態
以下のように書く。
@When("cond1")
こう書くとメソッドcond1が呼ばれ、それがtrueを返したときに限り、この@Whenアノテーションが付されたテストメソッドが実行される。
cond1が存在しない、publicでない、staticになっている、戻り値がbooleanではない、なんらかの引数をとる場合には、テストは速やかに失敗する。
ちなみに、@Whenアノテーションに与えられた文字列内のすべての空白文字は無視される。
これらの組み合わせ
上述の記法をもちいれば、利用可能な条件を組み合わせることで得られるすべての可能な論理式(またはそれに等価な論理式)を表現できる。
@When({"cond1&&cond2", "cond3", "!cond1&&cond4"})
これは
(cond1 && cond2) || cond3 || (!cond1&&cond4)
に対応する。
なお、各々の文字列の中ではカッコを使うことはできない。
まとめ
JCUnitのように複数のテストケースを自動的に生成するフレームワークでは、生成されたテストケースに応じて確認すべき内容が変化することがある。
このような場合分けを自動的に行う方法を導入した。
使い方の例を再掲すると以下のとおり。
@Test @When({ "aIsNonZero&&discriminantIsNonNegative&&coefficientsAreValid" }) public void whenSolveEquation$thenSolved() { whenSolveEquation(); // <-- この辺を人間が書く代わりにJCUnitが勝手に実行するようにしたらいいのでは? thenSolved(); // <-- }
今後
BDD的な応用については、いくつかアイデアがなくもないが、所詮は耳学問でその上、切実な必要も、その利益に対する実感も今のところあまりない。
現在のところは、github上でチケットをファイルするにとどめ、今後、作者の理解が進み興味が赴いた場合にちまちまと作ることにする。*1
読者のフィードバックを待つ次第である。
@Whenアノテーションについては、選言標準型に論理式を変換することで、いかなる論理式も表現可能であるとは言え、そうした手間をユーザにかけさせるのもいかがなものかというのはもっともな指摘である。
Antlrを使って、括弧をもちいたより複雑な論理式を解析させるのも可能だが、一方で汎用的なテストフレームワークであるJCUnitとしては、外部ライブラリへの依存はできるだけ持ちたくない。痛し痒しというところだ。*2
*1:https://github.com/dakusui/jcunit/issues/7
*2:ちなみにJCUnitがTupleの内容を文字列化するときに、JacksonもGSONも使っていないのはこのためだ
0.4.12リリース。
今朝、JCUnit 0.4.12をリリースした。
Maven coordinateは以下のとおり。
<dependency>
<groupId>com.github.dakusui</groupId>
<artifactId>jcunit</artifactId>
<version>0.4.12</version>
</dependency>
0.4.10->0.4.12での今回の変更点は、
1.の方は要するに一昨日エントリを書いてみると作ったものを客観的に見ることができるようになり、BDD的な意味合いからも通常の使用においても、@Whenの方が自然という考えにいたったためだ。ブログは書いてみるものだ。一昨日のエントリについては、後で訂正記事でもポストしようと思う。
2.の方は各因子にデフォルト値を定め、各因子の水準を一個ずつ変化させるものだ。むろん、ペアの網羅率は低いし、多くの場合はテストケースの数も多くなるだろう。が、単体テストの初期などではこちらの方が使いやすい場合も多かろう。実効速度も高いし。*1
他には動作に影響のある変更はない。
*1:ペアワイズのテスト生成は因子が100もあると10分20分は平気でかかる