前回 => テスト駆動開発を実践しながら Rust で xUnit を書いてみる(1)
はじめに
前回の続きで、20章と21章をやっていきます。今回は teardown とテスト実行数の話題です。
実装
後片付け(20章)
teardown を実装します。その前に、setup -> メソッド -> teardown の順番で実行されているか確認したいので、実行ログを WasRun
内で保持することにします。テストはこんな感じでしょうか。
impl TestCaseTest { fn test_running(&mut self) { self.test.run(); assert!(self.test.was_run); } fn test_setup(&mut self) { self.test.run(); assert_eq!(self.test.log, String::from("setup ")); } }
WasRun
を定義するところを全て変えていきます。
struct WasRun { was_run: bool, log: String, name: String, } impl TestCase for WasRun { fn new(name: String) -> WasRun { WasRun { was_run: false, log: String::from(""), name, } } fn run(&mut self) { self.setup(); match self.name.as_str() { "test_method" => self.test_method(), _ => (), } } } impl TestCase for TestCaseTest { fn new(name: String) -> TestCaseTest { TestCaseTest { name, test: WasRun { was_run: false, log: String::from(""), name: String::from("test_method"), }, } } fn run(&mut self) { match self.name.as_str() { "test_running" => self.test_running(), "test_setup" => self.test_setup(), _ => (), } } }
最後に setup
関数をフラグからログを使う方式に変えて完成です。
impl WasRun { fn setup(&mut self) { self.log += "setup "; } fn test_method(&mut self) { self.was_run = true; } }
メソッドが走ったかどうかを確かめるフラグもログに置き換えます。こちらもテストから書きます。
impl TestCaseTest { fn test_running(&mut self) { self.test.run(); assert!(self.test.was_run); } fn test_setup(&mut self) { self.test.run(); assert_eq!(self.test.log, String::from("setup test_method ")); } }
メソッドの動作を変えます。
impl WasRun { fn setup(&mut self) { self.log += "setup "; } fn test_method(&mut self) { self.log += "test_method "; } }
test_running
周りがいらなくなるのでそこに関連する部分をごっそり消して完成です。残された test_setup
は名前が不適当になったので test_template_method
にしておきます。メソッド名が変えたことによる match 式部分の変え忘れが怖いので、どれにもマッチしなかった時は雑ですがパニックするようにしておきます。
ここまでで書いたコードは次のようになります。
trait TestCase { fn new(name: String) -> Self; fn run(&mut self); } struct WasRun { log: String, name: String, } impl TestCase for WasRun { fn new(name: String) -> WasRun { WasRun { log: String::from(""), name, } } fn run(&mut self) { self.setup(); match self.name.as_str() { "test_method" => self.test_method(), _ => unreachable!("no match"), } } } impl WasRun { fn setup(&mut self) { self.log += "setup "; } fn test_method(&mut self) { self.log += "test_method "; } } struct TestCaseTest { name: String, test: WasRun, } impl TestCase for TestCaseTest { fn new(name: String) -> TestCaseTest { TestCaseTest { name, test: WasRun { log: String::from(""), name: String::from("test_method"), }, } } fn run(&mut self) { match self.name.as_str() { "test_template_method" => self.test_template_method(), _ => unreachable!("no match"), } } } impl TestCaseTest { fn test_template_method(&mut self) { self.test.run(); assert_eq!(self.test.log, String::from("setup test_method ")); } } fn main() { TestCaseTest::new(String::from("test_template_method")).run(); }
やっと teardown
のテストが書けます。
impl TestCaseTest { fn test_template_method(&mut self) { self.test.run(); assert_eq!(self.test.log, String::from("setup test_method teardown ")); } }
teardown
を実装し、run
の最後に実行するようにして終了です。
impl TestCase for WasRun { fn new(name: String) -> WasRun { WasRun { log: String::from(""), name, } } fn run(&mut self) { self.setup(); match self.name.as_str() { "test_method" => self.test_method(), _ => unreachable!("no match"), } self.teardown(); } } impl WasRun { fn setup(&mut self) { self.log += "setup "; } fn test_method(&mut self) { self.log += "test_method "; } fn teardown(&mut self) { self.log += "teardown "; } }
ちょっとしたリファクタリングとして、使っている TestCaseTest
が1つになり共通の WasRun
を持つ必要がなくなったので消してしまいます。
ここまでで20章の内容は終了です。コードは現在このようになっています。
trait TestCase { fn new(name: String) -> Self; fn run(&mut self); } struct WasRun { log: String, name: String, } impl TestCase for WasRun { fn new(name: String) -> WasRun { WasRun { log: String::from(""), name, } } fn run(&mut self) { self.setup(); match self.name.as_str() { "test_method" => self.test_method(), _ => unreachable!("no match"), } self.teardown(); } } impl WasRun { fn setup(&mut self) { self.log += "setup "; } fn test_method(&mut self) { self.log += "test_method "; } fn teardown(&mut self) { self.log += "teardown "; } } struct TestCaseTest { name: String, } impl TestCase for TestCaseTest { fn new(name: String) -> TestCaseTest { TestCaseTest { name } } fn run(&mut self) { match self.name.as_str() { "test_template_method" => self.test_template_method(), _ => unreachable!("no match"), } } } impl TestCaseTest { fn test_template_method(&self) { let mut test = WasRun { log: String::from(""), name: String::from("test_method"), }; test.run(); assert_eq!(test.log, String::from("setup test_method teardown ")); } } fn main() { TestCaseTest::new(String::from("test_template_method")).run(); }
数え上げ(21章)
複数のテストの実行結果を表示します。まずはべた書きでテストと実装を書いてしまいましょう。
impl TestCaseTest { fn test_template_method(&self) { let mut test = WasRun { log: String::from(""), name: String::from("test_method"), }; test.run(); assert_eq!(test.log, String::from("setup test_method teardown ")); } fn test_result(&self) { let mut test = WasRun { log: String::from(""), name: String::from("test_method"), }; let result = test.run(); assert_eq!(result.summary(), String::from("1 run, 0 failed")); } } fn main() { TestCaseTest::new(String::from("test_template_method")).run(); TestCaseTest::new(String::from("test_result")).run(); }
TestCase
トレイトを実装しているものには run
の度に何かしらの結果を返して欲しいので、新しく TestResult
という struct を用意してそれを返してもらうようにします。また TestResult
は summary
というメソッドが必要です。さらに run
が TestResult
を返すことをコードにも教えます。
struct TestResult {} impl TestResult { fn summary(&self) -> String { String::from("1 run, 0 failed") } } trait TestCase { fn new(name: String) -> Self; fn run(&mut self) -> TestResult; }
対応する部分を書き換えたらひとまず出来上がりです。
impl TestCase for WasRun { fn new(name: String) -> WasRun { WasRun { log: String::from(""), name, } } fn run(&mut self) -> TestResult { self.setup(); match self.name.as_str() { "test_method" => self.test_method(), _ => unreachable!("no match"), } self.teardown(); TestResult {} } } impl TestCase for TestCaseTest { fn new(name: String) -> TestCaseTest { TestCaseTest { name } } fn run(&mut self) -> TestResult { match self.name.as_str() { "test_template_method" => self.test_template_method(), "test_result" => self.test_result(), _ => unreachable!("no match"), } TestResult {} } }
べた書きの結果を直していきます。TestResult
が実行数を持てるようにしましょう。さらにテスト結果内の実行数をここから取るようにします。
struct TestResult { run_count: i16, } impl TestResult { fn summary(&self) -> String { format!("{} run, 0 failed", self.run_count) } }
このカウントを増やす処理を行っていないのでテストが落ちます。run
の度に TestResult
を用意して実行時にカウントを増やす処理を行うと上手く行きます。
impl TestResult { fn summary(&self) -> String { format!("{} run, 0 failed", self.run_count) } fn test_started(&mut self) { self.run_count += 1; } } impl TestCase for WasRun { fn new(name: String) -> WasRun { WasRun { log: String::from(""), name, } } fn run(&mut self) -> TestResult { self.setup(); let mut result = TestResult { run_count: 0 }; result.test_started(); match self.name.as_str() { "test_method" => self.test_method(), _ => unreachable!("no match"), } self.teardown(); result } } impl TestCase for TestCaseTest { fn new(name: String) -> TestCaseTest { TestCaseTest { name } } fn run(&mut self) -> TestResult { let mut result = TestResult { run_count: 0 }; result.test_started(); match self.name.as_str() { "test_template_method" => self.test_template_method(), "test_result" => self.test_result(), _ => unreachable!("no match"), } result } }
ここでそれぞれ実装している run
メソッドは両方 setup
-> TestResult
の用意と起動 -> match での実行メソッド分岐 (Python での getattr) -> teardown
-> TestResult
の返却(setup
と teardown
は何もしない可能性あり)という構造になっているので、この流れを踏まえてトレイトに引き上げます。
trait TestCase { fn new(name: String) -> Self; fn setup(&mut self); fn teardown(&mut self); fn getattr(&mut self); fn run(&mut self) -> TestResult { self.setup(); let mut result = TestResult { run_count: 0 }; result.test_started(); self.getattr(); self.teardown(); result } } impl TestCase for WasRun { fn new(name: String) -> WasRun { WasRun { log: String::from(""), name, } } fn setup(&mut self) { self.log += "setup "; } fn teardown(&mut self) { self.log += "teardown "; } fn getattr(&mut self) { match self.name.as_str() { "test_method" => self.test_method(), _ => unreachable!("no match"), } } } impl TestCase for TestCaseTest { fn new(name: String) -> TestCaseTest { TestCaseTest { name } } fn setup(&mut self) {} fn teardown(&mut self) {} fn getattr(&mut self) { match self.name.as_str() { "test_template_method" => self.test_template_method(), "test_result" => self.test_result(), _ => unreachable!("no match"), } } }
テストが壊れている時も確認したいので、テストを追加します。
impl TestCaseTest { fn test_failed_result(&self) { let mut test = WasRun { log: String::from(""), name: String::from("broken_method") }; let result = test.run(); assert_eq!(result.summary(), String::from("1 run, 1 failed")); } } fn main() { TestCaseTest::new(String::from("test_template_method")).run(); TestCaseTest::new(String::from("test_result")).run(); TestCaseTest::new(String::from("test_failed_result")).run(); }
getattr
の修正と壊れている必ずパニックするメソッドの追加を行います。
impl WasRun { fn test_method(&mut self) { self.log += "test_method "; } fn broken_method(&mut self) { panic!("panic"); } }
この詳しい実装は次章になるので、一度対応するテストをコメントアウトしてここは終了です。コード全体は以下のようになっています。
struct TestResult { run_count: i16, } impl TestResult { fn summary(&self) -> String { format!("{} run, 0 failed", self.run_count) } fn test_started(&mut self) { self.run_count += 1; } } trait TestCase { fn new(name: String) -> Self; fn setup(&mut self); fn teardown(&mut self); fn getattr(&mut self); fn run(&mut self) -> TestResult { self.setup(); let mut result = TestResult { run_count: 0 }; result.test_started(); self.getattr(); self.teardown(); result } } struct WasRun { log: String, name: String, } impl TestCase for WasRun { fn new(name: String) -> WasRun { WasRun { log: String::from(""), name, } } fn setup(&mut self) { self.log += "setup "; } fn teardown(&mut self) { self.log += "teardown "; } fn getattr(&mut self) { match self.name.as_str() { "test_method" => self.test_method(), "broken_method" => self.broken_method(), _ => unreachable!("no match in WasRun"), } } } impl WasRun { fn test_method(&mut self) { self.log += "test_method "; } fn broken_method(&mut self) { panic!("panic"); } } struct TestCaseTest { name: String, } impl TestCase for TestCaseTest { fn new(name: String) -> TestCaseTest { TestCaseTest { name } } fn setup(&mut self) {} fn teardown(&mut self) {} fn getattr(&mut self) { match self.name.as_str() { "test_template_method" => self.test_template_method(), "test_result" => self.test_result(), "test_failed_result" => self.test_failed_result(), _ => unreachable!("no match in TestCaseTest"), } } } impl TestCaseTest { fn test_template_method(&self) { let mut test = WasRun { log: String::from(""), name: String::from("test_method"), }; test.run(); assert_eq!(test.log, String::from("setup test_method teardown ")); } fn test_result(&self) { let mut test = WasRun { log: String::from(""), name: String::from("test_method"), }; let result = test.run(); assert_eq!(result.summary(), String::from("1 run, 0 failed")); } fn test_failed_result(&self) { let mut test = WasRun { log: String::from(""), name: String::from("broken_method") }; let result = test.run(); assert_eq!(result.summary(), String::from("1 run, 1 failed")); } } fn main() { TestCaseTest::new(String::from("test_template_method")).run(); TestCaseTest::new(String::from("test_result")).run(); // TestCaseTest::new(String::from("test_failed_result")).run(); }
終わりに
少しずつ難しくなってきましたが、試行錯誤の工程などもなかなか楽しめています。