TL; DR
JUnitライクなコードが書ける気がします。
speculate - Cargo: packages for Rust
https://crates.io/crates/speculateutkarshkukreti/speculate.rs: An RSpec inspired minimal testing framework for Rust.
https://github.com/utkarshkukreti/speculate.rs
$ cargo test -- --nocapture --test-threads=1
Compiling speculate-sample v0.1.0 (/home/hoge/workspace/speculate-sample)
Finished dev [unoptimized + debuginfo] target(s) in 0.53s
Running target/debug/deps/speculate_sample-2003a589699fd299
running 5 tests
test speculate_0::nest::and_nest::test_is_fifth_test ...
before
before
before
test5
after
after
after
ok
test speculate_0::nest::and_nest::test_is_fourth_test ...
before
before
before
test4
after
after
after
ok
test speculate_0::nest::test_is_second_test ...
before
before
test2
after
after
ok
test speculate_0::nest::test_is_third_test ...
before
before
test3
after
after
ok
test speculate_0::test_is_first_test ...
before
test1
after
ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
fn main() {
println!("Hello, world!");
}
# [cfg(test)]
extern crate speculate;
# [cfg(test)]
use speculate::speculate;
# [cfg(test)]
speculate! {
before {
println!("\nbefore");
}
after {
println!("after");
}
it "is first test" {
println!(" test1");
}
describe "math" {
before {
println!(" before");
}
after {
println!(" after");
}
it "is second test" {
println!(" test2");
}
it "is third test" {
println!(" test3");
}
describe "arithmetic" {
before {
println!(" before");
}
after {
println!(" after");
}
it "is fourth test" {
println!(" test4");
}
it "is fifth test" {
println!(" test5");
}
}
}
}
はじめに
Rustでテストコードを書こうと思った時、皆さんも一度は「before文が使いたい!」と思ったことがあるでしょう。
しかし、Rustに標準で存在するコードでは、beforeやafterという概念はありません。
これで困ってしまうのはJUnit出身者です。
Rustはそもそも所有権を1つしか持つことができないため、テストメソッドごとにオブジェクトを生成するという方法は理にかなっています。
しかし、どうにも面倒です。
私はただ、テスト対象オブジェクトとテストに必要なオブジェクトを毎回生成したいだけなのに......!
mod tests {
use super::*;
#[test]
fn add_obj1_and_obj2() {
let mut obj1 = Obj1::new();
let mut obj2 = Obj2::new();
let obj3 = Obj3::new();
let obj4 = Obj4::new();
let mut obj5 = Obj5::new();
let expected = 1*3 + 2*4;
let actual = obj5.add(obj1.set_obj3(&obj3), obj2.set_obj4(&obj4));
assert_eq!(expected, actual);
}
#[test]
fn add_obj1_and_obj3() {
let mut obj1 = Obj1::new();
let obj2 = Obj2::new();
let mut obj3 = Obj3::new();
let obj4 = Obj4::new();
let mut obj5 = Obj5::new();
let expected = 1*2 + 3*4;
let actual = obj5.add(obj1.set_obj2(&obj2), obj3.set_obj4(&obj4));
assert_eq!(expected, actual);
}
#[test]
fn add_obj1_and_obj4() {
let mut obj1 = Obj1::new();
let obj2 = Obj2::new();
let obj3 = Obj3::new();
let mut obj4 = Obj4::new();
let mut obj5 = Obj5::new();
let expected = 1*2 + 4*3;
let actual = obj5.add(obj1.set_obj2(&obj2), obj4.set_obj3(&obj3));
assert_eq!(expected, actual);
}
#[test]
fn add_obj2_and_obj3() {
let obj1 = Obj1::new();
let mut obj2 = Obj2::new();
let mut obj3 = Obj3::new();
let obj4 = Obj4::new();
let mut obj5 = Obj5::new();
let expected = 2*1 + 3*4;
let actual = obj5.add(obj2.set_obj1(&obj1), obj3.set_obj4(&obj4));
assert_eq!(expected, actual);
}
// まだまだ続く
}
解決策
speculate.rsというモジュールがいい感じに使えそうです。
speculate - Cargo: packages for Rust
https://crates.io/crates/speculateutkarshkukreti/speculate.rs: An RSpec inspired minimal testing framework for Rust.
https://github.com/utkarshkukreti/speculate.rs
利用方法などは、READMEに全て書いてあります。
こちらでも簡単に説明します。
導入方法
Cargo.tomlファイルの dev-dependencies
にspeculateを追加します。
[dev-dependencies]
speculate = "0.1"
テストを記述するファイルにspeculateの利用宣言をします。
# [cfg(test)]
extern crate speculate;
# [cfg(test)]
use speculate::speculate; // テスト記述ファイルに必須
使い方
speculate!
マクロでテストコードを囲みます。
次の項目を利用できます。
-
describe
,context
: テストコードのグループ化 -
before
: テストコード実行前の設定など -
after
: テストコード実行後の修復など -
it
,test
: テストコード -
bench
: ベンチマークテスト
また、次のオプションを利用できます。
-
#[ignore]
: テストコードを無視 -
#[should_panic]
: panicしてほしいテスト -
#[should_panic(expected = "foo")]
: panic時にfooを出力してほしいテスト
実際に使ってみる
上記のコードは、下記のようになります。
テストコードにおける初期化文が短くなり、テストコードの中身がわかりやすくなりました。
# [cfg(test)]
extern crate speculate;
# [cfg(test)]
use speculate::speculate;
# [cfg(test)]
speculate! {
use super::*;
before {
let mut obj1 = Obj1::new();
let mut obj2 = Obj2::new();
let mut obj3 = Obj3::new();
let mut obj4 = Obj4::new();
let mut obj5 = Obj5::new();
}
it "add obj1 and obj2" {
let expected = 1*3 + 2*4;
let actual = obj5.add(obj1.set_obj3(&obj3), obj2.set_obj4(&obj4));
assert_eq!(expected, actual);
}
it "add obj1 and obj3" {
let expected = 1*2 + 3*4;
let actual = obj5.add(obj1.set_obj2(&obj2), obj3.set_obj4(&obj4));
assert_eq!(expected, actual);
}
it "add obj1 and obj4" {
let expected = 1*2 + 4*3;
let actual = obj5.add(obj1.set_obj2(&obj2), obj4.set_obj3(&obj3));
assert_eq!(expected, actual);
}
it "add obj2 and obj3" {
let expected = 2*1 + 3*4;
let actual = obj5.add(obj2.set_obj1(&obj1), obj3.set_obj4(&obj4));
assert_eq!(expected, actual);
}
}
Tips
利用時に気になった点を記します。
warning: variable does not need to be mutable
before文でobjを生成する際に、各テストコードで1箇所でもミュータブルな利用をしている場合は、mutを付ける必要があります。
上記の例を用いると、 add_obj1_and_obj2
ではobj3とobj4が、 add_obj1_and_obj3
ではobj2とobj4がイミュータブルですが、obj2とobj3はどちらかでミュータブルであるため、before文では両方ミュータブル属性を付ける必要があります。
before {
let obj1 = Obj1::new();
// obj2, obj3, obj4は全てミュータブルにする
let mut obj2 = Obj2::new();
let mut obj3 = Obj3::new();
let mut obj4 = Obj4::new();
let mut obj5 = Obj5::new();
}
it "add obj1 and obj2" {
let expected = 1*3 + 2*4;
// obj3とobj4はイミュータブルでよい
let actual = obj5.add(obj1.set_obj3(&obj3), obj2.set_obj4(&obj4));
assert_eq!(expected, actual);
}
it "add obj1 and obj3" {
let expected = 1*2 + 3*4;
// obj2とobj4はイミュータブルでよい
let actual = obj5.add(obj1.set_obj2(&obj2), obj3.set_obj4(&obj4));
assert_eq!(expected, actual);
}
さて、上記の場合でテストを実行すると、 warning: variable does not need to be mutable
が出現します。
warning: variable does not need to be mutable
--> src/main.rs
|
| let mut obj3 = Obj3::new();
| ----^
| |
| help: remove this `mut`
|
= note: #[warn(unused_mut)] on by default
warning: variable does not need to be mutable
--> src/main.rs
|
| let mut obj2 = Obj2::new();
| ----^
| |
| help: remove this `mut`
恐らくですが、beforeおよびafterは、すべてのテストコードの上部で実行しているため、 add_obj1_and_obj2()
ではobj3が、 add_obj1_and_obj3()
ではobj2が、それぞれWarningを出力している気がします。
Rustのコンパイラは素晴らしいですが、ここでは結構うっとおしいため、 before文に #[allow(unused_mut)]
をつければ消えます。
これはRustの流儀に反するかもしれません。
あまり大声で言うのはやめましょう。
before {
let obj1 = Obj1::new();
// obj2, obj3, obj4は全てミュータブルにする
#[allow(unused_mut)]
let mut obj2 = Obj2::new();
#[allow(unused_mut)]
let mut obj3 = Obj3::new();
let mut obj4 = Obj4::new();
let mut obj5 = Obj5::new();
}
it "add obj1 and obj2" {
// ...
}
it "add obj1 and obj3" {
// ...
}
テストコード名で日本語を利用するには
speculate.rsモジュールではテストコード名を文字列で表現しています。
日本語が使えそうですが、そのままでは使えません。
#![feature(non_ascii_idents)]
を使えと言われます。
これは、テストコード名をそのまま関数名に流用しているためです。
他にも、大文字を利用するとワーニングが出力されたりします。
気を付けましょう。
とは言え、日本語が使えるのはありがたいですね。
JUnitでは日本語テストコードをバンバン書いていたため、日本語テストコードが書けないテストコードはつらいです。
# ![feature(non_ascii_idents)]
# [cfg(test)]
extern crate speculate;
# [cfg(test)]
use speculate::speculate;
# [cfg(test)]
speculate! {
use super::*;
before {
// ...
}
test "obj1とobj2を加算する" {
let expected = 1*3 + 2*4;
let actual = obj5.add(obj1.set_obj3(&obj3), obj2.set_obj4(&obj4));
assert_eq!(expected, actual);
}
// ...
}
non_ascii_idents - Rust: The Usable Book
https://doc.rust-lang.org/unstable-book/language-features/non-ascii-idents.html
おわりに
Rustでテストコードを書く際のモジュールとして、speculate.rsを紹介しました。
既存のテストコードとはちょっと書き方が異なるため、変更はちょっと大変だと思います。
しかし、新しいテストコードを書く際には、使ってみてはいかがでしょうか。