前回、思いついた方法でやったけど、詰んだという話を書いた。
TDD を実施するために Moq を試してみた その2実践してみた
前回は師匠の言う通りと思って、お題の KeyVaultHelper の Mock はあきらめるという方針をとって、それはとても正解と思うが、プログラミングテクニック的に他のやり方で出来ないかを考えて実践したらうまくいったのでブログに書き留めておきたい。金で解決する方法と、シンプルな方法の二つをご紹介します。
師匠の一言
私の尊敬するプログラミング師匠があの後コメントをくれた。そういえば以前師匠はこういっていた。
C# での Mock は、Moq で。お金でぶん殴るなら、Fakesで
「お金でぶん殴る」というのはどういう意味かというと、Fakes という Mockツールは、Visual Studio Enterprise でしか使えない機能らしいのだ。マイクロソフトの私は使えるじゃないか、、、というわけでどんなものか興味もあったので、実装して試してみた。
Fakes
Fakes は Microsoft の出しているテストのモックツールです。次のサイトが公式のものです。
他に具体的な例はこちらがわかりやすいです。
この Fakes は何のためにあるかというと、Moq を使ってて実際にあるのですが、Mock が出来るかどうかは元のクラスやインターフェイスの設計に依存します。だから、自分でいじれないライブラリを Mock しようとしたときに、virtual がついてなくて、オーバーライドできなかったり、インターフェイスが無かったり、拡張メソッドだったりすると、あきらめるしかない、、、となってしまいます。自分がライブラリに手を入れられない状況でも、Mock をしたいのが元々の動機のようです。
その Mock の方法ですが、衝撃的です。C# の DLL を書き換えて、Mock 用 DLL を作るという方法です。Fakes は、Visual Studio Enterprise で、References を右クリックして、Mock 対象のライブラリをAdd Fakes Assembly
を選択すると、偽物のDLLが生成されます。このDLL をテストのプロジェクトからも、参照すると、使えるようになります。かなり大胆ですね。
先ほどのブログに紹介されていた図ですが、とんでもないものまで Mock できます。Static メソッドに、private メソッドなんでもありです。ただ、実際にやってもそうですが、パフォーマンスは遅いです。
実際に昨日のコードに対して Fakes でテストを書くとちゃんと成功しました。Fakes には、Stub というモードと、Shims というモードがあると思いますが、実際に Fakes が生きるのは、Shims なので、そっちでの要素が多そうです。ポイントを解説したいと思います。
[TestMethod]
public async Task TestGetSecretByShim()
{
var identifiyer = "https://abc.vault.azure.net/secrets/";
var secretValue = "bar";
using (ShimsContext.Create()) // 1. ShimsContext の設定
{
ShimSecretBundle.AllInstances.ValueGet = (c) => secretValue; // 2. SecretBundle のValue メソッドの Mock
Microsoft.Azure.KeyVault.Fakes.ShimKeyVaultClientExtensions.GetSecretAsyncIKeyVaultClientStringCancellationToken = (c,s,ca) => Task.FromResult(new SecretBundle());
ShimKeyVaultClient.ConstructorKeyVaultCredentialHttpClient = (a, b, c) => { }; // 3. KeyVaultClientExtensionのGetSecretAsyncメソッドの Mock
var helper = new KeyVaultHelper(client: new KeyVaultClient((KeyVaultCredential)null, (HttpClient) null), vaultBaseUrl: identifiyer);
Assert.AreEqual("bar", await helper.GetSecretValueAsync("foo"));
}
}
1. ShimsContext の設定
このShimsContext の範囲で、Shims の Mock が有効になります。
2. SecretBundle の Value メソッドのMockちなみに、
クラス名が、Shimが頭についた名前に変更されていて、全てのインスタンスメソッドの中で、Get するものという感じで、クラス名もメソッド名も変更されています。インテリセンスがあるので、.
を押して、使えるメソッドを眺めていると、どれが自分がモックしたいメソッドなのかがわかると思います。
3. KeyVaultCientExensions の GetSecretAsync のMock
ノリは 2. と同じですが、引数の型が、ことごとくメソッド名に加えられています。IKeyVaultClient でもなく、KeyVaultClient でもなく、KeyVaultClientExtensions という名前になっています。これは何かというと、KeyVaultClient の GetSecretAsync というメソッドは、「拡張メソッド」と呼ばれるものです。元の KeyVaultClient には定義がなく、後で拡張されたものです。KeyVaultClient リファレンス Mixin を実現するのに使われます。
これが、何が問題かというと、Moq では、この拡張メソッドの Mock ができません。なぜなら、拡張メソッドは、static メソッドを、インスタンスメソッドのように呼び出しているものだからです。拡張メソッド ただ、Shims はそんなものもお構いなく Mock します。だから、この方法でばっちりテストが通りました。
おおお!まさに、出来ないことを可能にする、人間を超越した気分です!ついにできたぞー!
しかし、意味は薄い
盛り上がっておいてなんですが、これを Mock したところでなんの意味があるのかはよくわかりません。Mock のテストの重要なポイントは自分で書いたロジックをテストすることであります。だから、テストとしては、Mock したところ以外のロジックがちゃんと動いているか、Mock に想定した値が渡っているかがテストのポイントですが、この方式だと、結果を Mock しているので、どちらもテストできていません。全くの自己満足ですw
ただ、Fakes 自体は不可能を可能にする仕組みですので、こいつさえ Mock できればぁーーーきーーーー。となっているときには救世主になるかもしれません。試せてよかった。
じゃあ、Moq と Fakes を組み合わせればいいんじゃないの?いや、Moqだけで行けるかもと思って書いたのがこのコード
var identifiyer = "https://abc.vault.azure.net/secrets/";
var secretName = "foo";
var keyVaultClientMock = new Mock<IKeyVaultClient>();
var sb = new SecretBundle();
sb.Value = "bar";
keyVaultClientMock.Setup(c => c.GetSecretAsync($"{identifiyer}{secretName}/", new CancellationToken())).Returns(Task.FromResult(sb));
var helper = new KeyVaultHelper(keyVaultClientMock.Object, "https://abc.vault.azure.net");
Assert.AreEqual("bar", await helper.GetSecretValueAsync("foo"));
残念ながら結論としてはできません。先で書いた通り、GetSecretAsync メソッドは、拡張メソッドなので、Moq では Mock 出来ないのです。ぐぬぬ。はやりあきらめるしかないのか、、、。
金で殴らない単純な解決策
そんな時にふとむっちゃ簡単な解決策を思いつきました。
- そもそも、Mock したいのは、KeyVaultClient のしかも一つのメソッドだけ。
- テストしたいのは、GetSecretValueAsyncメソッド内で書いている、baseUrl + /secrets/ + keyの名前 というロジックがちゃんと実行されているか?ということだけ
なので`KeyVaultClientの.GetSecretAsyncを実行している部分+戻り値で問題になる、SecretBundleクラスのモックをあきらめて、SecretBundle から、Value を取り出すメソッドのテストだけをあきらめて、メソッドに切り出してあげればいいということです。
これを
public async Task<string> GetSecretValueAsync(string name)
{
var identifier = $"{vaultBaseUrl}/secrets/{name}/";
var secret = await keyClient.GetSecretAsync(identifier);
return secret.Value;
}
こんな感じに
public async virtual Task<string> GetSecretVelueWithIdentifierAsync(string identifier)
{
var secret = await keyClient.GetSecretAsync(identifier);
return secret.Value;
}
public async Task<string> GetSecretValueAsync(string name)
{
var identifier = $"{vaultBaseUrl}/secrets/{name}/";
return await GetSecretVelueWithIdentifierAsync(identifier);
}
テストはこんな感じで
public async Task TestGetAsyncByMoqOnly()
{
var baseUrl = "https://abc.vault.azure.net";
var secretName = "foo";
var keyVaultHelperMock = new Mock<KeyVaultHelper>(null, baseUrl);
keyVaultHelperMock.Setup(c => c.GetSecretVelueWithIdentifierAsync($"{baseUrl}/secrets/{secretName}/")).ReturnsAsync("bar");
Assert.AreEqual("bar", await keyVaultHelperMock.Object.GetSecretValueAsync("foo"));
}
昨日はあきらめでいいやと思ったのですが、このクラスをコーディングしていくと、どうしても、KeyValueHelper自体をテストしたい場面が出てきて、この方法をふと思いつきました。よくよく考えたら前から知ってる方法じゃないか、、、何をやってたのよ、、、 orz
ちなみにツイッターでご指摘いただいた次の方法で、Moq の 戻り値の書き方のおしゃれさが増しています。
むっちゃありがとうございました!
しかし、これですっきりテストも通りました!しかも、この方法だと、テストしたい部分(ロジック)がしっかりテストされているし、適切な値が渡ってきたときしか、このMock が発動しないので、Moq の Velify を使わなくても、Mock したメソッドに正しい値が渡ってきたことが保証できています。これ一番ええやん。金つかってないけど!人間を超越しなくても、出来るやん!素晴らしい!人間賛歌だ!
おわりに
いろんなコメントをいただきありがとうございました。皆様のおかげでこの解放にたどり着けました。
今日はいろいろ学んだのでまだまだブログしようと思います。