LoginSignup
7
4

More than 3 years have passed since last update.

Rustでテストするならspeculate.rsを使うといいかもしれない

Posted at

TL; DR

JUnitライクなコードが書ける気がします。

speculate - Cargo: packages for Rust
https://crates.io/crates/speculate

utkarshkukreti/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
main.rs
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/speculate

utkarshkukreti/speculate.rs: An RSpec inspired minimal testing framework for Rust.
https://github.com/utkarshkukreti/speculate.rs

利用方法などは、READMEに全て書いてあります。

こちらでも簡単に説明します。

導入方法

Cargo.tomlファイルの dev-dependencies にspeculateを追加します。

Cargo.toml
[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を出力してほしいテスト

実際に使ってみる

上記のコードは、下記のようになります。
テストコードにおける初期化文が短くなり、テストコードの中身がわかりやすくなりました。

speculate.rsを利用したテストコード
#[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
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の流儀に反するかもしれません。
あまり大声で言うのはやめましょう。

allow(unused_mu)
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を紹介しました。
既存のテストコードとはちょっと書き方が異なるため、変更はちょっと大変だと思います。
しかし、新しいテストコードを書く際には、使ってみてはいかがでしょうか。

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4