かべブログ

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

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

はじめに

言わずと知れたテスト駆動開発を今更読んでいたところ、筆者が「新しい言語に触れるときは xUnit の実装から始めてみる」という旨のことを書いていたので、最近触れている Rust の勉強と TDD の練習を兼ねてやってみることにしました。「テスト駆動開発」の第2部を読みながらご覧ください。

開発環境

  • cargo, rustc 1.66.0

実装

xUnit へ向かう小さな一歩(18章)

まずやりたいのは以下のようなテストです。

  1. 構造体を初期化
  2. メソッドがまだ走っていないことを確認
  3. メソッドを走らせる
  4. メソッドが走ったことを確認

Rust で単純に書いてみるとこんな感じでしょうか。

fn main() {
    let mut test = WasRun::new(String::from("test_method"));
    assert!(!test.was_run);
    test.run();
    assert!(test.was_run);
}

当然 WasRun が無いと怒られるので、足します。また WasRun::new() で初期化したいのでそれも実装してあげます。

struct WasRun {
    was_run: bool,
}

impl WasRun {
    fn new(name: String) -> WasRun {
        WasRun { was_run: false }
    }
}

最後に test_method とそれを呼ぶ run を実装して出来上がり、、

impl WasRun {
    fn new(name: String) -> WasRun {
        WasRun { was_run: false }
    }

    fn test_method(&self) {
        self.was_run = true;
    }

    fn run(&self) {
        self.test_method();
    }
}

と思いきや test.test_method() で怒られています。test の中身を変えているので let mut で定義する必要がありました。これを解決すると次のようになり完成です。cargo run も通ることを確かめてみてください。

struct WasRun {
    was_run: bool,
}

impl WasRun {
    fn new(name: String) -> WasRun {
        WasRun { was_run: false }
    }

    fn test_method(&mut self) {
        self.was_run = true;
    }

    fn run(&mut self) {
        self.test_method();
    }
}

fn main() {
    let mut test = WasRun::new(String::from("test_method"));
    assert!(!test.was_run);
    test.run();
    assert!(test.was_run);
}

Pythongetattr による動的な呼び出しは Rust で対応する方法が見つからなかったので、少し力技で実装を行います。match 文で対応する関数を呼び出すように変更します。(そもそも実装方針を変えた方が良いかもしれませんが、ここは本準拠で行きます。)

impl WasRun {
    fn new(name: String) -> WasRun {
        WasRun { was_run: false, name }
    }

    fn test_method(&mut self) {
        self.was_run = true;
    }

    fn run(&mut self) {
        match self.name.as_str() {
            "test_method" => self.test_method(),
            _ => (),
        }
    }
}

リファクタリングを行います。newrun メソッドはこれからの実装でもよく使いそうなので分割させたいのですが、Rust ではクラスもなく継承もできないので、トレイトで実現します。

trait TestCase {
    fn new(name: String) -> Self;
    fn run(&mut self);
}

struct WasRun {
    was_run: bool,
    name: String,
}

impl TestCase for WasRun {
    fn new(name: String) -> WasRun {
        WasRun { was_run: false, name }
    }

    fn run(&mut self) {
        match self.name.as_str() {
            "test_method" => self.test_method(),
            _ => (),
        }
    }
}

impl WasRun {
    fn test_method(&mut self) {
        self.was_run = true;
    }
}

最後にテスト実行用の struct を、さっき実装したトレイトを使って作りましょう。テストの中身をこの struct の中に入れることができます。

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_running" => self.test_running(),
            _ => (),
        }
    }
}

impl TestCaseTest {
    fn test_running(&self) {
        let mut test = WasRun::new(String::from("test_method"));
        assert!(!test.was_run);
        test.run();
        assert!(test.was_run);
    }
}

そうするとテストの実行部分はこのように1行で書くことが出来ます。

fn main() {
    TestCaseTest::new(String::from("test_running")).run();
}

これでこの章の実装は完了です。現在のコードは次のようになっています。

trait TestCase {
    fn new(name: String) -> Self;
    fn run(&mut self);
}

struct WasRun {
    was_run: bool,
    name: String,
}

impl TestCase for WasRun {
    fn new(name: String) -> WasRun {
        WasRun { was_run: false, name }
    }

    fn run(&mut self) {
        match self.name.as_str() {
            "test_method" => self.test_method(),
            _ => (),
        }
    }
}

impl WasRun {
    fn test_method(&mut self) {
        self.was_run = true;
    }
}

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_running" => self.test_running(),
            _ => (),
        }
    }
}

impl TestCaseTest {
    fn test_running(&self) {
        let mut test = WasRun::new(String::from("test_method"));
        assert!(!test.was_run);
        test.run();
        assert!(test.was_run);
    }
}

fn main() {
    TestCaseTest::new(String::from("test_running")).run();
}

前準備(19章)

テスト前に行われる動作 (セットアップ) に関する実装を行います。まずは例のごとくテストから書きます。

impl TestCaseTest {
    fn test_running(&self) {
        let mut test = WasRun::new(String::from("test_method"));
        assert!(!test.was_run);
        test.run();
        assert!(test.was_run);
    }
    fn test_setup(&self) {
        let mut test = WasRun::new(String::from("test_method"));
        test.run();
        assert!(test.was_setup);
    }
}

fn main() {
    TestCaseTest::new(String::from("test_running")).run();
    TestCaseTest::new(String::from("test_setup")).run();
}

必要なフィールドを追加して、was_setup を変更するためのメソッドを追加します。

struct WasRun {
    was_run: bool,
    was_setup: bool,
    name: String,
}

impl WasRun {
    fn setup(&mut self) {
        self.was_setup = true;
    }

    fn test_method(&mut self) {
        self.was_run = true;
    }
}

run の実行時に setup を走らせれば OK です。

impl TestCase for WasRun {
    fn new(name: String) -> WasRun {
        WasRun {
            was_run: false,
            was_setup: false,
            name,
        }
    }

    fn run(&mut self) {
        self.setup();
        match self.name.as_str() {
            "test_method" => self.test_method(),
            _ => (),
        }
    }
}

このあと書籍内ではセットアップの動作は TestCaseTest でも行うということで TestCase に持たせていますが、実装を進めていたところ今回は new で struct を作るときに型付けに従って厳密に作っているため、TestCaseTest で setup を実装するメリットを今のところ感じられませんでした。よって setupTestCase トレイトに乗せることはしません。

またここまでの TestCaseTest 内のメソッドでは同じ let mut test: WasRun が使われているので、これを TestCaseTest の初期化に組み込みます。

struct TestCaseTest {
    name: String,
    test: WasRun,
}

impl TestCase for TestCaseTest {
    fn new(name: String) -> TestCaseTest {
        TestCaseTest {
            name,
            test: WasRun {
                was_run: false,
                was_setup: false,
                name: String::from("test_method"),
            },
        }
    }

    fn run(&mut self) {
        match self.name.as_str() {
            "test_running" => self.test_running(),
            "test_setup" => self.test_setup(),
            _ => (),
        }
    }
}

すると各テストの実行時の実装を少しシンプルに出来ます。

impl TestCaseTest {
    fn test_running(&mut self) {
        self.test.run();
        assert!(self.test.was_run);
    }
    fn test_setup(&mut self) {
        self.test.run();
        assert!(self.test.was_setup);
    }
}

これで19章の内容は終了です。ここまでのコードをまとめると以下のようになります。

trait TestCase {
    fn new(name: String) -> Self;
    fn run(&mut self);
}

struct WasRun {
    was_run: bool,
    was_setup: bool,
    name: String,
}

impl TestCase for WasRun {
    fn new(name: String) -> WasRun {
        WasRun {
            was_run: false,
            was_setup: false,
            name,
        }
    }

    fn run(&mut self) {
        self.setup();
        match self.name.as_str() {
            "test_method" => self.test_method(),
            _ => (),
        }
    }
}

impl WasRun {
    fn setup(&mut self) {
        self.was_setup = true;
    }

    fn test_method(&mut self) {
        self.was_run = true;
    }
}

struct TestCaseTest {
    name: String,
    test: WasRun,
}

impl TestCase for TestCaseTest {
    fn new(name: String) -> TestCaseTest {
        TestCaseTest {
            name,
            test: WasRun {
                was_run: false,
                was_setup: false,
                name: String::from("test_method"),
            },
        }
    }

    fn run(&mut self) {
        match self.name.as_str() {
            "test_running" => self.test_running(),
            "test_setup" => self.test_setup(),
            _ => (),
        }
    }
}

impl TestCaseTest {
    fn test_running(&mut self) {
        self.test.run();
        assert!(self.test.was_run);
    }
    fn test_setup(&mut self) {
        self.test.run();
        assert!(self.test.was_setup);
    }
}

fn main() {
    TestCaseTest::new(String::from("test_running")).run();
    TestCaseTest::new(String::from("test_setup")).run();
}

終わりに

TDD + Rust で xUnit を実装し始めてみました。やはりテストが先にあると設計方針の決定や自分の実装に安心感が持てる気がします。にここから少し難しくなりそうですが何とか続けていきます。

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

参考文献