かべブログ

TypeScript, React, Goなどでまったり個人開発。たまに競プロ。

テスト駆動開発を実践しながら Rust で xUnit を書いてみる(2)

前回 => テスト駆動開発を実践しながら 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 を用意してそれを返してもらうようにします。また TestResultsummary というメソッドが必要です。さらに runTestResult を返すことをコードにも教えます。

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 の返却(setupteardown は何もしない可能性あり)という構造になっているので、この流れを踏まえてトレイトに引き上げます。

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();
}

終わりに

少しずつ難しくなってきましたが、試行錯誤の工程などもなかなか楽しめています。

参考文献