shaku は DI ライブラリです。 日本語での情報が少なかったの詳細めに書いていきます。
mockall はモックライブラリです。 こちらは日本語での情報もすぐに見つかったので詳細は触れていないです。
まず shaku を使う
以降のコードスニペットにおいて use は省略しています。末尾にあるおまけの全体コードでは省略していないので気になる方はそちらを参照してください。 shaku の使い方については公式のガイドを見た方が当然詳しいですが、ここでも一通り使い方を書きます。
shaku を使うの最小の例
ここでは Hoge という trait で例示します。 trait を shaku で扱えるようにするには Interface の subtrait にします。
1 | pub trait Hoge: Interface { |
trait 側の準備はこれで完了です。
次に Hoge を実装した struct HogeImpl を用意します。 中身はてきとーです。 この HogeImpl の attribute に shaku(interface = Hoge) を指定することで、HogeImpl が Hoge の解決に利用されることを示します。 そして derive(Component) することでボイラープレートを自動生成します。 Component の代わりに Provider を使うこともできますがそれは後述します。
1 |
|
次に macro module! を使って依存解決に使う struct を作ります。
MyModule は自由に名前をつける部分です。 components と providers に、依存解決にどの struct を使うのかを指定します。Component である HogeImpl を components に指定することで Hoge が解決できるようになります。
1 | module! { |
これで使う準備は完了です。 テスト内で使ってみると以下のようになります。
1 |
|
Component Hoge を依存に持つ例
上の例だと Hoge として HogeImpl を作っているだけなので何も嬉しくないです。 次に Hoge に依存する別のものを作ってみます。
1 | pub trait Fuga: Interface { |
今回は FugaImpl を Component ではなく Provider としてみました。 2つの違いはざっくり以下です。
- Component は
MyModuleのインスタンス内において1つのインスタンスを共有する - Provider は
module.provide()の呼び出しの度に(内部での依存解決の度にも)新しくインスタンスが作られる
Component に依存する場合は Arc<dyn Trait> なフィールドを用意したうえで、その attribute に shaku(inject) を指定します。
module! 部分は以下のようになります。Provider の場合は providers に指定します。
1 | module! { |
テスト内で使ってみると以下のようになります。
1 |
|
Provider の場合は module.provide() でインスタンスを作って Box<dyn Trait> で受け取ります。
ここでは unwrap していますが derive(Provider) で生成した分についてはエラーにはなり得ないです。
Provider を手動で実装して provide 内でエラーになり得るコードを書いた場合はエラーの可能性があります。
Provider Fuga を依存に持つ例
Component だけでなく Provider への依存も持つことができます。 上で作った Fuga を依存に持つものを作ってみます。
1 | pub trait Piyo: Interface { |
Provider に依存する場合は Arc ではなく Box<dyn Trait> をフィールドに持たせて attribute shaku(provide) を付与します。 ちなみにProvider から Component に依存はできますが、逆の Component から Provider への依存はできません。
module! の定義にも追加したら利用できるようになります。
1 | module! { |
特定の実装を置き換える
上で作ったうち、他から依存されている Hoge と Fuga の実装をテストにおいて置き換えてみます。 今回は mockall を使うので trait の attribute に cfg_attr(test, automock) を指定しておきます。 これによって MockHoge と MockFuga が自動で作られます。
1 |
|
Component の実装を置き換える
テスト内で MockHoge を作って hoge が呼び出されたときに本来の実装とは別の値を返すようにしてみます。
1 | let mut mock_hoge = MockHoge::new(); |
module builder において with_component_override を使って Hoge の上書きをする設定を行ないます。
1 | let module = MyModule::builder() |
この module を使って依存解決を行なうとHoge として mock_hoge が使われるようになります。 実行結果を見ると mock_hoge が使われていることがわかります。
1 | let fuga: Box<dyn Fuga> = module.provide().unwrap(); |
Provider の実装を置き換える
Provider を置き換えるには module builder の with_provider_override を使います。 これの引数は Fn になっているので、クロージャ外で mock_fuga を作って move で渡しても所有権の問題(Fn は複数回呼ばれうるので返り値にするものが Clone できないと外から渡せない)でコンパイルが通りません。 なのでクロージャ内で作っています。
1 | let module = MyModule::builder() |
こちらのパターンでも別の実装に置き換えることができました。
1 | let piyo: Box<dyn Piyo> = module.provide().unwrap(); |
まとめというか感想
- shaku を使って DI まわりのボイラープレートを最小限にできた
- shaku 自体は非常に小さいライブラリなのでもしも問題があっても自分で対処できそう
- shaku と mockall の組み合わせで特に問題になることもなく普通に使えた
おまけ
最終形のコードの全体は以下です。
1 | use std::sync::Arc; |























