🎅

Rust vs. Go: Effective Unit Testing

2023/12/12に公開

Retail AI Adventurers Advent Calendar 2023 の投稿です。

https://qiita.com/advent-calendar/2023/rai-adventurers

Retail AI は、トライアルカンパニー を軸とした小売におけるお客様の買い物体験の向上を目指す企業です。

この投稿では、本業(SRE)のかたわらで取り組む Backend Tech Stack について書きます。

題材は、「Rust 初心者として、Standard な Test Code の実装方法」についてです。

Rust における Test Code の書き方と Go で一般的な Table Driven Tests[1] を使った Test Code について書きます。

tl;dr

  • Rust でも Go と同じような Table Driven Tests[1:1] を実装できます。
  • Rust では、compile 時に型チェックを行うため、Test Case の設計もより厳密になります。
  • Rust では、Unit tests と Integration tests で実装方法が異なります。
  • この投稿とは関係ありませんが、同日に別のアドカレ[2] に投稿しています。

How to write tests: Rust

Unit tests and Integration tests

Rust では、Unit tests と Integration tests で実装方法が異なります。

Unit tests

logic 内に書きます。
Test Code であることを明示するために、Annotation cfg(test) を付与します。

以下は、Official Docs より。

Unit Tests
The purpose of unit tests is to test each unit of code in isolation from the rest of the code to quickly pinpoint where code is and isn’t working as expected. You’ll put unit tests in the src directory in each file with the code that they’re testing. The convention is to create a module named tests in each file to contain the test functions and to annotate the module with cfg(test).

ユニットテストの目的は、コードの各ユニットを他のコードから切り離してテストし、コードが期待通りに動いているところと動いていないところを素早く突き止めることです。ユニットテストは、テストするコードのある各ファイルの src ディレクトリに置きます。各ファイルの中にtestsという名前のモジュールを作成し、テスト関数を格納し、cfg(test)というアノテーションをつけるのが慣例です。

Integration tests

tests directory に書きます。

以下は、Official Docs より。

Integration Tests
In Rust, integration tests are entirely external to your library. They use your library in the same way any other code would, which means they can only call functions that are part of your library’s public API. Their purpose is to test whether many parts of your library work together correctly. Units of code that work correctly on their own could have problems when integrated, so test coverage of the integrated code is important as well. To create integration tests, you first need a tests directory.

Rustでは、統合テストは完全にライブラリの外部です。つまり、ライブラリのパブリックAPIの一部である関数のみを呼び出すことができます。統合テストの目的は、ライブラリの多くの部分が正しく連携して動作するかどうかをテストすることです。単体では正しく動作するコードも、統合すると問題が発生する可能性があります。統合テストを作成するには、まず tests ディレクトリが必要です。

adder
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests                            # Here
    └── integration_test.rs          # Here

https://doc.rust-lang.org/book/ch11-03-test-organization.html?highlight=cfg(test)#unit-tests

Unit tests について、詳しく見ていきます。

まず、Unit tests は、cfg(test) を付与するのが慣例です。

以下、sample code です。

https://github.com/danny-yamamoto/rust-api-samples/blob/e1062ae0c951e3be676d2ab8fb61341893437fe7/main/src/routes.rs#L105-L116

Unit Tests を実行します。cargo test

vscode ➜ /workspaces/rust-api-samples/main (main) $ cargo test
   Compiling main v0.1.0 (/workspaces/rust-api-samples/main)
    Finished test [unoptimized + debuginfo] target(s) in 1m 33s
     Running unittests src/main.rs (target/debug/deps/main-794ccb24f64c7f92)

running 3 tests
test routes::tests::err_api_response_users ... ok
test routes::tests::test_api_response_users ... ok
test routes::users_service_tests::test_fetch_users ... ok      # Here

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s

vscode ➜ /workspaces/rust-api-samples/main (main) $

Table Driven Tests in Rust

Table Driven Tests[1:2] を Rust で実践するとどうなるか。
これは、諸元があるわけではなく、個人的に考えた実装方法です。

Vector 型の配列にテストケースを用意します。

https://github.com/danny-yamamoto/rust-api-samples/blob/e1062ae0c951e3be676d2ab8fb61341893437fe7/main/src/routes.rs#L118-L142

vscode ➜ /workspaces/rust-api-samples/main (main) $ cargo test
   Compiling main v0.1.0 (/workspaces/rust-api-samples/main)
    Finished test [unoptimized + debuginfo] target(s) in 1m 36s
     Running unittests src/main.rs (target/debug/deps/main-794ccb24f64c7f92)

running 4 tests
test routes::tests::err_api_response_users ... ok
test routes::tests::test_api_response_users ... ok
test routes::users_service_tests::test_fetch_users ... ok
test routes::users_service_tests::tdt_fetch_users ... ok             # Here

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.28s

vscode ➜ /workspaces/rust-api-samples/main (main) $ 

How to write tests: Go

Go の test は、言わずと知れた Table Driven Tests[1:3] です。
mock[3] として、database を先に用意します。
setupMockDB で初期データを投入するのもあり。もしくは、db ファイルをコミットしておきます。

https://github.com/danny-yamamoto/go-api-samples/blob/2958a4e8919e5b55f2c164a24e7a6346cd5d07d4/internal/users/users_test.go#L18-L43

今回実装した Code は、以下です。

https://github.com/danny-yamamoto/rust-api-samples/tree/main/main

https://github.com/danny-yamamoto/go-api-samples

簡単ですが、Test Code については以上です。

この投稿をみて何か得られた方は、いいね ❤️ をお願いします。

それでは、次回のアドカレでお会いしましょう。👋

BTW

この投稿とは関係ありませんが、同日に別のアドカレに投稿しています。

CCoEクリスマス!クラウド技術を活用して組織をカイゼンした事例を投稿しよう! by KINTOテクノロジーズ Advent Calendar 2023

https://qiita.com/advent-calendar/2023/kinto-technlogies

2023年11月に GA になった合成モニターについて書いています。

もしよろしければ、こちらも見ていただけると嬉しいです。

脚注
  1. https://dave.cheney.net/2019/05/07/prefer-table-driven-tests ↩︎ ↩︎ ↩︎ ↩︎

  2. https://qiita.com/advent-calendar/2023/kinto-technlogies ↩︎

  3. ユニットテストで使われるテクニックの一つです。システムの一部分(例えば外部サービスやデータベース)を模擬する「モック オブジェクト」を作成し、本物のオブジェクトの代わりに使用します。 ↩︎

Discussion