shaku は DI ライブラリです。 日本語での情報が少なかったの詳細めに書いていきます。
mockall はモックライブラリです。 こちらは日本語での情報もすぐに見つかったので詳細は触れていないです。
まず shaku を使う
以降のコードスニペットにおいて use
は省略しています。末尾にあるおまけの全体コードでは省略していないので気になる方はそちらを参照してください。 shaku の使い方については公式のガイドを見た方が当然詳しいですが、ここでも一通り使い方を書きます。
shaku を使うの最小の例
ここでは Hoge
という trait で例示します。 trait を shaku で扱えるようにするには Interface
の subtrait にします。
1 2 3
| pub trait Hoge: Interface { fn hoge(&self) -> String; }
|
trait 側の準備はこれで完了です。
次に Hoge
を実装した struct HogeImpl
を用意します。 中身はてきとーです。 この HogeImpl
の attribute に shaku(interface = Hoge)
を指定することで、HogeImpl
が Hoge
の解決に利用されることを示します。 そして derive(Component)
することでボイラープレートを自動生成します。 Component
の代わりに Provider
を使うこともできますがそれは後述します。
1 2 3 4 5 6 7 8 9
| #[derive(Component)] #[shaku(interface = Hoge)] pub struct HogeImpl;
impl Hoge for HogeImpl { fn hoge(&self) -> String { "hoge".to_string() } }
|
次に macro module!
を使って依存解決に使う struct を作ります。
MyModule
は自由に名前をつける部分です。 components
と providers
に、依存解決にどの struct を使うのかを指定します。Component である HogeImpl
を components
に指定することで Hoge
が解決できるようになります。
1 2 3 4 5 6
| module! { MyModule { components = [HogeImpl], providers = [], } }
|
これで使う準備は完了です。 テスト内で使ってみると以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #[cfg(test)] mod tests { use shaku::HasComponent;
use super::*;
#[test] fn test_hoge() { let module = MyModule::builder().build(); let hoge: &dyn Hoge = module.resolve_ref();
let result = hoge.hoge(); assert_eq!(result, "hoge"); } }
|
Component Hoge
を依存に持つ例
上の例だと Hoge
として HogeImpl
を作っているだけなので何も嬉しくないです。 次に Hoge
に依存する別のものを作ってみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| pub trait Fuga: Interface { fn fuga(&self) -> String; }
#[derive(Provider)] #[shaku(interface = Fuga)] pub struct FugaImpl { #[shaku(inject)] hoge: Arc<dyn Hoge>, }
impl Fuga for FugaImpl { fn fuga(&self) -> String { format!("fuga: {}", self.hoge.hoge()) } }
|
今回は FugaImpl
を Component
ではなく Provider
としてみました。 2つの違いはざっくり以下です。
- Component は
MyModule
のインスタンス内において1つのインスタンスを共有する
- Provider は
module.provide()
の呼び出しの度に(内部での依存解決の度にも)新しくインスタンスが作られる
Component
に依存する場合は Arc<dyn Trait>
なフィールドを用意したうえで、その attribute に shaku(inject)
を指定します。
module!
部分は以下のようになります。Provider の場合は providers
に指定します。
1 2 3 4 5 6
| module! { MyModule { components = [HogeImpl], providers = [FugaImpl], } }
|
テスト内で使ってみると以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #[cfg(test)] mod tests { use shaku::{HasProvider};
use super::*;
#[test] fn test_fuga() { let module = MyModule::builder().build(); let fuga: Box<dyn Fuga> = module.provide().unwrap();
let result = fuga.fuga(); assert_eq!(result, "fuga: hoge"); } }
|
Provider の場合は module.provide()
でインスタンスを作って Box<dyn Trait>
で受け取ります。
ここでは unwrap
していますが derive(Provider)
で生成した分についてはエラーにはなり得ないです。
Provider を手動で実装して provide
内でエラーになり得るコードを書いた場合はエラーの可能性があります。
Provider Fuga
を依存に持つ例
Component だけでなく Provider への依存も持つことができます。 上で作った Fuga
を依存に持つものを作ってみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| pub trait Piyo: Interface { fn piyo(&self) -> String; }
#[derive(Provider)] #[shaku(interface = Piyo)] pub struct PiyoImpl { #[shaku(provide)] fuga: Box<dyn Fuga>, }
impl Piyo for PiyoImpl { fn piyo(&self) -> String { format!("piyo: {}", self.fuga.fuga()) } }
|
Provider に依存する場合は Arc
ではなく Box<dyn Trait>
をフィールドに持たせて attribute shaku(provide)
を付与します。 ちなみにProvider から Component に依存はできますが、逆の Component から Provider への依存はできません。
module!
の定義にも追加したら利用できるようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| module! { MyModule { components = [HogeImpl], providers = [FugaImpl, PiyoImpl], } }
#[cfg(test)] mod tests { use shaku::HasProvider;
use super::*;
#[test] fn test_piyo() { let module = MyModule::builder().build(); let piyo: Box<dyn Piyo> = module.provide().unwrap();
let result = piyo.piyo(); assert_eq!(result, "piyo: fuga: hoge"); } }
|
特定の実装を置き換える
上で作ったうち、他から依存されている Hoge
と Fuga
の実装をテストにおいて置き換えてみます。 今回は mockall
を使うので trait の attribute に cfg_attr(test, automock)
を指定しておきます。 これによって MockHoge
と MockFuga
が自動で作られます。
1 2 3 4 5
| #[cfg_attr(test, automock)] pub trait Hoge: Interface { ... }
#[cfg_attr(test, automock)] pub trait Fuga: Interface { ... }
|
Component の実装を置き換える
テスト内で MockHoge
を作って hoge
が呼び出されたときに本来の実装とは別の値を返すようにしてみます。
1 2 3 4
| let mut mock_hoge = MockHoge::new(); mock_hoge .expect_hoge() .returning(|| "mocked_hoge".to_string());
|
module builder において with_component_override
を使って Hoge
の上書きをする設定を行ないます。
1 2 3
| let module = MyModule::builder() .with_component_override::<dyn Hoge>(Box::new(mock_hoge)) .build();
|
この module
を使って依存解決を行なうとHoge
として mock_hoge
が使われるようになります。 実行結果を見ると mock_hoge
が使われていることがわかります。
1 2 3
| let fuga: Box<dyn Fuga> = module.provide().unwrap(); let result = fuga.fuga(); assert_eq!(result, "fuga: mocked_hoge");
|
Provider の実装を置き換える
Provider を置き換えるには module builder の with_provider_override
を使います。 これの引数は Fn
になっているので、クロージャ外で mock_fuga
を作って move
で渡しても所有権の問題(Fn
は複数回呼ばれうるので返り値にするものが Clone
できないと外から渡せない)でコンパイルが通りません。 なのでクロージャ内で作っています。
1 2 3 4 5 6 7 8 9
| let module = MyModule::builder() .with_provider_override::<dyn Fuga>(Box::new(|_| { let mut mock_fuga = MockFuga::new(); mock_fuga .expect_fuga() .returning(|| "mocked_fuga".to_string()); Ok(Box::new(mock_fuga)) })) .build();
|
こちらのパターンでも別の実装に置き換えることができました。
1 2 3
| let piyo: Box<dyn Piyo> = module.provide().unwrap(); let result = piyo.piyo(); assert_eq!(result, "piyo: mocked_fuga");
|
まとめというか感想
- shaku を使って DI まわりのボイラープレートを最小限にできた
- shaku 自体は非常に小さいライブラリなのでもしも問題があっても自分で対処できそう
- shaku と mockall の組み合わせで特に問題になることもなく普通に使えた
おまけ
最終形のコードの全体は以下です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
| use std::sync::Arc;
use shaku::{module, Component, Interface, Provider};
#[cfg(test)] use mockall::{automock, predicate::*};
#[cfg_attr(test, automock)] pub trait Hoge: Interface { fn hoge(&self) -> String; }
#[derive(Component)] #[shaku(interface = Hoge)] pub struct HogeImpl;
impl Hoge for HogeImpl { fn hoge(&self) -> String { "hoge".to_string() } }
#[cfg_attr(test, automock)] pub trait Fuga: Interface { fn fuga(&self) -> String; }
#[derive(Provider)] #[shaku(interface = Fuga)] pub struct FugaImpl { #[shaku(inject)] hoge: Arc<dyn Hoge>, }
impl Fuga for FugaImpl { fn fuga(&self) -> String { format!("fuga: {}", self.hoge.hoge()) } }
#[cfg_attr(test, automock)] pub trait Piyo: Interface { fn piyo(&self) -> String; }
#[derive(Provider)] #[shaku(interface = Piyo)] pub struct PiyoImpl { #[shaku(provide)] fuga: Box<dyn Fuga>, }
impl Piyo for PiyoImpl { fn piyo(&self) -> String { format!("piyo: {}", self.fuga.fuga()) } }
module! { MyModule { components = [HogeImpl], providers = [FugaImpl, PiyoImpl], } }
#[cfg(test)] mod tests { use shaku::{HasComponent, HasProvider};
use super::*;
#[test] fn test_hoge() { let module = MyModule::builder().build(); let hoge: &dyn Hoge = module.resolve_ref();
let result = hoge.hoge(); assert_eq!(result, "hoge"); }
#[test] fn test_fuga() { let module = MyModule::builder().build(); let fuga: Box<dyn Fuga> = module.provide().unwrap();
let result = fuga.fuga(); assert_eq!(result, "fuga: hoge"); }
#[test] fn test_fuga_mock() { let mut mock_hoge = MockHoge::new(); mock_hoge .expect_hoge() .returning(|| "mocked_hoge".to_string());
let module = MyModule::builder() .with_component_override::<dyn Hoge>(Box::new(mock_hoge)) .build(); let fuga: Box<dyn Fuga> = module.provide().unwrap();
let result = fuga.fuga(); assert_eq!(result, "fuga: mocked_hoge"); }
#[test] fn test_piyo() { let module = MyModule::builder().build(); let piyo: Box<dyn Piyo> = module.provide().unwrap();
let result = piyo.piyo(); assert_eq!(result, "piyo: fuga: hoge"); }
#[test] fn test_piyo_mock() { let module = MyModule::builder() .with_provider_override::<dyn Fuga>(Box::new(|_| { let mut mock_fuga = MockFuga::new(); mock_fuga .expect_fuga() .returning(|| "mocked_fuga".to_string()); Ok(Box::new(mock_fuga)) })) .build(); let piyo: Box<dyn Piyo> = module.provide().unwrap();
let result = piyo.piyo(); assert_eq!(result, "piyo: mocked_fuga"); } }
|