Rust の shaku と mockall を組み合わせて使う例

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) を指定することで、HogeImplHoge の解決に利用されることを示します。 そして 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 は自由に名前をつける部分です。 componentsproviders に、依存解決にどの struct を使うのかを指定します。Component である HogeImplcomponents に指定することで 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())
}
}

今回は FugaImplComponent ではなく 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");
}
}

特定の実装を置き換える

上で作ったうち、他から依存されている HogeFuga の実装をテストにおいて置き換えてみます。 今回は mockall を使うので trait の attribute に cfg_attr(test, automock) を指定しておきます。 これによって MockHogeMockFuga が自動で作られます。

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");
}
}