Rustコトハジメ

プログラミング言語Rustと競プロに関する情報をお届けします。

test-generatorを使ってテストを自動生成する

自動テストを始めることにした経緯

ijkを開発していて、ツイッターでバグレポをもらった。

f:id:akiradeveloper529:20190320105257j:plain

ありえん。そうならないように設計されているはずだ。以前にも確かめたはずだ。しかし今のコードで試してみると確かに異常終了するのであった。

これに懲りて、私はエディタの自動テストを作ることにした。

自動テストの重要性

テストを自動化することの重要性については言うまでもないけど、OSSに自動テストがついてることは、私はとても重要なことだと考えている。

テストは、開発を安全に進めるためのものでもあるが、ユーザになり得る人にとって、そのプロジェクトがまともなものかどうか判断する重要な材料ともなる。これは、テストされているということ自体もそうだし、開発者自体がテストに対して理解があるか、どういう考え方を持っているかを知れるからだ。ソフトウェアがテスタブルな良い設計になっていることの証明にもなる。テスタブルでない設計は、大抵まずい。なぜまずいかというと、開発者自体がソフトウェアを抽象的に捉えられていないからだ。

まともなOSSプロジェクトではしっかりとしたテストがついている。しかしそれ以外では、ちょこっとしたサンプルがついていてそれでテストと言い張っていたり、一体これで何を保証しようとしているのかよくわからないテストをしている場合もあり、混沌としている。

何をテストするか

フレームグラフを使ったボトルネック検出

www.rustforbeginners.com

をしたのと同じように、ファイルの初期状態を決めて、それに対してキー入力の列を与えることにする。そして今回はテストなので、期待する状態と一致するか確かめる。

testcases/
  test_name_a/
    input
    keys
    output
  test_name_b/
  ...

このようにテストケースを追加していくだけで、それに対してテストコードを生成して実行してくれるようにする。この良い点は、Rustのテストコードを送るのは億劫なユーザがテストケースをPRすることが出来るようになることだ。

これはRakeを使ってテストをする場合などは良くやることだが、Rustはコンパイル言語なので話は少し難しくなる。

test-generatorライブラリを使う

私が調査した限りでは、

GitHub - frehberg/test-generator

が求めるものにもっとも近いと思った。

例に書いてあることだが、

extern crate test_generator;
#[cfg(test)]
mod tests { 
    test_generator::test_expand_paths! { test_exists; "data/*" }
    fn test_exists(dir_name: &str) { assert!(std::path::Path::new(dir_name).exists()); }
}

のようなコードを書くと、テスト用の関数を自動生成してくれる。

mod tests {
    #[test]
    fn test_exists_data_set1() {
        test_exists("data/set1");
    }
    #[test]
    fn test_exists_data_set2() {
        test_exists("data/set2");
    }
}

テストファイル追加を検知する

しかし、このテストコードはコンパイル時に生成されるため、テストファイルを追加したとしても、コード自体をリコンパイルしない限り、新しいテストコードは生成されないことになる。

外部ファイルの変化に追従してコンパイルを行う仕組みが必要だ。test-generatorの作者が作っているbuild-depsというライブラリを併せて使うことによって、外部ファイルの変更を検知してリコンパイルすることが出来るようになる。

[package]
...
build = "build.rs"

[build-dependencies]
build-deps = "^0.1"
// build.rs
extern crate build_deps;
fn main() {
    build_deps::rerun_if_changed_paths( "behavior/*" ).unwrap();
    build_deps::rerun_if_changed_paths( "behavior" ).unwrap();
}
test editor::tests::test_editor_behavior_empty_x ... ok
test editor::tests::test_editor_behavior_x_remove_char ... ok
test editor::tests::test_editor_behavior_dd_lastline ... ok

これで期待通りのものが出来る。同じようなことをしたい人の参考になればと思う。