Rustでmockするならmockallで決まり!・・・でよろしいでしょうか?
Rustで DI (Dependency Injection)、してますか?
今日話題にするのはドメイン層でインターフェイスを定義してインフラ層でその実装を書くやつです。
例えばドメイン層で trait UserRepository
を書いて、インフラ層で struct UserRepositoryImpl
するやつです。
テストを書くとき、 struct UserRepositoryImpl
はDBアクセスなどしてしまうので取り回しが悪いから、mock を作って fixture を入出力したいことありますよね。
Rustでそういうことやるなら mockall がオススメだよという記事です。
そんなに不満はないのですが、もしベターなやり方があったら記事末尾のコメントやTwitterやらもらえたら嬉しいです。
前職のFOLIO時代の同僚で現CADDiの むらみんさんの記事 に
外部通信のような比較的大きい副作用が絡むテストに於いて テストダブルを差し込むことは可能なのですが、かなりの労力が必要になる印象を持っています。
と書いていたのを今更ながら発見して、自分はこうしてるけど皆はどうしてるんだろ?と思って筆(キーボード)を取りました。
目次
- mockall 紹介の題材
- コードレベルのアーキテクチャ
- mockall の使い方について解説
- おわりに
- おまけ: rust-analyzer を使っていて
use ...::MockFoo
が unresolved import エラーになるとき
mockall 紹介の題材
mockallを紹介するためにクリーンアーキテクチャなアプリケーションを用意しました。
簡易的なメアド帳です。動かし方は README.md に書いています。
コードレベルのアーキテクチャ
domain
: Enterprise Business Rules- Entity, Value Object, リポジトリインターフェイスを置いています。
app
: Application Business Rules- UseCaseと、
tests
以下にUseCaseのブラックボックステストを置いています。
ここのテストで、mockall で自動生成したリポジトリインターフェイスのモックを使っています。
- UseCaseと、
interface-adapter
: Interface Adapters- ControllerやDTO (Data Transfer Object) を置いています。
infra
: Frameworks & Drivers- UIとしてCLI実装を置いています。
- リポジトリ実装として、永続化層にYAMLファイルを使ったものを置いています。
mockall の使い方について解説
app/tests
のテストにおいてモックを使っているので、 app
と domain
層だけの解説になります。interface-adapter
層と infra
層は興味があればコードを見てみてください。
UserRepository, UseCase 実装まで
まず、 User
の一覧・作成・更新を担当する UserRepository
を作ります。永続化の方法などは infra
層に任せたいので、 domain
層に trait として作ります。
1 | pub trait UserRepository { |
エラー型の詳細はコードを読んでみてください。シンプルなインターフェイスです。
ユースケースにおいて、このアプリケーションの機能を列挙していきます。
1 |
|
ユースケースの各種機能を実現する一連の処理を記述すると、リポジトリを使うことになります。
例として UseCase::search_users()
の実装をなんとなく書いてみましょう。
1 |
|
UserRepository::list()
を呼び出す必要がありますね。
そのためには UseCase
が UserRepository
のインスタンスを得られる必要があります。
より正確には、 UserRepository
は trait なので、型パラメーターを使って <R: UserRepository>
のインスタンスが必要です。
ここでは struct UseCase
のフィールドとして持たせることにします。
1 |
|
UserRepository
の各関連関数は &self
しか取らない (self
を取らない) ので、 UseCase
が持つのは <R: UserRepository>
の所有権ではなく参照で十分です。
結果として 'r
というライフタイムパラメータも必要になってちょっと煩わしいですが、これがRustです。
Tipsですが、 Rc
とか使うと struct の中のライフタイムパラメータを避けられるのでライフタイムパラメータで頭痛がしてきたときは使ってしまいます。
さて、 UseCase
が user_repo
フィールドを持つようになったので、 UseCase::search_users
実装のコメントアウトしていた部分が書けます。
1 |
|
Tips: domain層の trait を関連型でまとめた trait を作っておくと、型パラメータの数が減らせて便利
mockall の紹介という意味では不要なのですが、Rustでクリーンアーキテクチャするときに個人的に便利だと思っているTipsです。
今回 domain
層に trait UserRepository
を置いてありますが、実際のアプリケーションだともっとたくさんの trait が出てくるはずです。infra
層ではその実装が全部出揃うので苦労しませんが、 domain
, app
, interface-adapter
では domain
層の trait の型パラメーターだらけになるの、経験があるのではないでしょうか?
1 | struct UseCase<UserRepo: UserRepository, ItemRepo: ItemRepository, ...> { |
みたいな感じで…
型パラメーターを一本化するために、おまとめ trait を domain
層に置いておくと便利に感じます。このアプリケーションのコードにおいては以下のようなものを置いています。
1 | /// UseCaseなどの各所で都度同じような型パラメータを定義しないで済むように、リポジトリtraitをこのtraitの関連型としてまとめる。 |
今回は使用している trait が UserRepository
1つなので若干ありがたみに書けますが、コメントにお気持ちを書いています。
有用そうなら真似してみてください。
UseCase 実装を完成させてテストを書こうとしてみる
UseCase
のコンストラクタはこのように書けます。
1 | impl<'r, R: Repositories> UseCase<'r, R> { |
その他の関数も含めて完成させたものが app/src/use_case.rs です。
ユースケースは複雑度も高い部分なので念入りにテストしたいですね。今回は app/tests
以下に、 UseCase::search_users()
関数のブラックボックステストを書くことにします。
1 |
|
「 UserRepository
が空っぽの場合はどんなクエリを投げても検索結果は空」ということをテストしています。
ここで空っぽの UserRepository
を作るためにモックを作りたいですね。
愚直にやるとこんな感じでしょうか。
1 | struct EmptyUserRepository; |
そしてこれを使い、おまとめ trait 実装も作ります。
1 | pub struct EmptyRepositories { |
これがあれば、テストコードのコメントアウト部分も埋められます。
1 |
|
これでできる!できるのですが!以下の点が気に掛かります。
UseCase::search_users()
が内部的に叩いていないUserRepository::create()
,UserRepository::update()
にまでunimplemented!()
を書いて回る必要がある。- やりたい動作(今回は「空っぽのユーザーリストを返す」)の数だけモックの
struct
を作る必要がある。- しかも今回はおまとめ trait も作っているので、おまとめ trait の数も増える。
mockall ならこれらの悩みを解決してくれます。
mockall を使って UserRepository::list()
をモックする
詳細な使い方は ドキュメント を参照してください。ここでは自分の使い方を小ネタ含めてお伝えします。
先程は struct EmptyUserRepository
を手書きしましたが、mockall を使うと trait にアノテーションを書けば MockUserRepository
をマクロで自動実装してくれます。
1 | # ... |
1 |
|
自動実装された MockUserRepository
をおまとめ trait に設定します。モックの挙動は都度自由に差し替えられるので、今回は EmptyRepositories
という挙動を表す名前はやめて、 TestRepositories
という汎用的な名前にします。
1 | pub struct TestRepositories { |
TestRepositories
, MockUserRepository
を使ってテストコ−ドを書いていきます。
1 | use domain::user::user_repository::MockUserRepository; |
これを実際に走らせると、 MockUserRepository::list(): No matching expectation found
というランタイムエラーになります。
これは「 list()
関数のモック挙動が挿されていないぞ」という意味です。
空っぽのユーザーリストを返す挙動を挿しましょう。 テストコードのどこでも、クロージャーを使って挙動を差し替えられる のが便利ポイントです。
1 | use domain::user::user_repository::MockUserRepository; |
これでテストケースの1個目が完成です。
Fixture を使って複数の User を返すリポジトリのモック実装を作る
1 | MockUserRepository::new().expect_list().returning(|| /* お好みのUserリスト */); |
の形式でいろいろな状態のリポジトリを簡単にモック実装できることがを紹介しました。
いろいろな状態のリポジトリを作るには色々な User
が必要なので、fixture を作っておくと便利です。
実際に作った fixture はこちらです。User
を変数の形で定義するのではなく、 User
を返す関数を定義しているのがともすると特徴的に感じるかと思いますが、以下のような理由です。
- Rustの基本機能でグローバル変数のようなものを作ろうとすると
const
かstatic
を使うが、いずれも基本的にはUser
のようなプリミティブではない型を定義できない。 - once_cell などを使えば
static
でグローバル変数を作れるが、mockall が提供する.expect_YOUR_METHOD().returning(|| /* ... */)
のクロージャーの中でstatic
変数を使おうとすると、Copy
実装がされていない限りは都度.clone()
していかないと使えなかったりして取り回しが面倒。
このように些細な理由ではありますが、 fixture は関数形式で作っておくことをおすすめします。
横道に逸れましたが、この fixture を使って「ユーザーを3種類返すリポジトリ」のモック実装をし、その環境における UseCase::search_users()
の挙動のテストをします。
1 | use domain::user::user_repository::MockUserRepository; |
モック実装が必要かを依存先に選ばせる
先程 domain
において
1 |
|
と書きましたが、この書き方だと domain
crate に依存する crate は、テストも書かないかもしれないのに MockUserRepository
が見えた状態になってしまいます。
気になる場合は Cargo の Features を使って制御すると良いでしょう。
1 | # ... |
1 | # ... |
1 |
|
このようにすれば、 app
において dev-dependencies
が有効になる cargo test
のときなどだけ MockUserRepository
がリンクされます。
おわりに
今回は mockall の使い方の中でも、
- 引数を取らない関数のモック
- 自分自身で実装した trait にアノテーションを書く形式の自動モック実装
を紹介しました。この他に使ったことのある便利機能として、
などありますので、ドキュメントを一読いただきよろしければ使ってみてください。
おまけ: rust-analyzer を使っていて use ...::MockFoo
が unresolved import エラーになるとき
VSCodeで使っていると、 use ...::MockFoo
をしているファイルを開いているときに rust-analyzer が unresolved import エラーを報告します。
https://github.com/rust-analyzer/rust-analyzer/issues/6038 で報告されている問題と同根であると考えられ、issue内のコメントに従いVSCodeの場合は設定ファイルに
1 | "rust-analyzer.diagnostics.disabled": ["unresolved-import"] |
を追記すれば解消します。