先日学んだ Moq を使って、外部呼出しバリバリのコードを TDD するべく実装してみました。これは実際の PoC のプロジェクトで書いてみた簡単なコードです。まだ私は C# に練度がないので、TDD で回すことができていません。Moqをマスターして、TDDでないと体が受け付けなくするぞ!
1. テスタビリティ戦略
これが最初のコードです。いろいろ「不吉なにおい」がしますが、まずはテスタビリティを高めるための戦略を考えてみます。Mock の対象となりそうなのは、KeyVauleClient です。この中身をテストしても仕方がないし、実際 Azure 上にある KeyVault へのアクセスも避けたいところです。あと、KeyVaultClient のセットアップ部分は定型コードですし、ここを苦労してテストを書いてもあまり実りがないでしょう。
using Microsoft.Azure.KeyVault;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.Threading.Tasks;
namespace KeyVault
{
public class KeyVaultHelper
{
private KeyVaultClient keyClient;
private string vaultBaseUrl;
public KeyVaultHelper()
{
const string applicationId = "SOME APPLICATION ID";
const string applicationSecret = "SOME APPLICATION SECRET";
const string vaultBaseUrl = "https://xxxxx.vault.azure.net";
SetUp(applicationId, applicationSecret, vaultBaseUrl);
}
private void SetUp(string applicationId, string applicationSecret, string vaultBaseUrl)
{
this.vaultBaseUrl = vaultBaseUrl;
keyClient = new KeyVaultClient(async (authority, resource, scope) =>
{
var adCredential = new ClientCredential(applicationId, applicationSecret);
var authenticationContext = new AuthenticationContext(authority, null);
return (await authenticationContext.AcquireTokenAsync(resource, adCredential)).AccessToken;
});
}
public async Task<string> GetSecretValueAsync(string name)
{
var identifier = $"{vaultBaseUrl}/secrets/{name}/";
var secret = await keyClient.GetSecretAsync(identifier);
return secret.Value;
}
}
}
私が考えたのは2つの方法で、
- KeyVaultClient と、vaultBaseUrl を渡すコンストラクタを作る
- KeyVaultClient を 渡せる Public メソッドをつくる (C# は Package Private がないらしい)
という感じ。先日やった Autofac となんとなく相性がよさそうだし、シンプルなので、コンストラクタをチョイス。実際にコードを書いてみよう。
2. テストコードの実装
Visual Studio を使ったテストコードは、プロジェクトを作って、プロジェクトを右クリックして、Build Dependencies -> Project Dependencies で、プロジェクト間の依存を設定します。
その後、References > Add References で同様に、依存関係を設定します。
ついでに Live UnitTests > Include を設定して、準備完了。
3. テストコードの記述
今回は非同期のライブラリをモックします。基本は、KeyVaultClient のモックと、KeyVaultClient の戻り値の、Mock です。ここでは、SecretBundle
というクラスのバンドル。このコードを書くのも結構いろいろ格闘しましたので、そのポイントを解説したいと思います。
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Microsoft.Azure.KeyVault;
using System.Threading.Tasks;
using Microsoft.Azure.KeyVault.Models;
using System.Threading;
using KeyVault;
namespace KeyVault.Test
{
[TestClass]
public class KeyVaultHlperTest
{
[TestMethod]
public async Task TestGetSecret()
{
var identifiyer = "https://abc.vault.azure.net/secrets/";
var secretName = "foo";
var secretMock = new Mock<SecretBundle>();
secretMock.Setup(sec => sec.Value).Returns("bar");
var keyVaultClientMock = new Mock<KeyVaultClient>();
keyVaultClientMock.Setup(c => c.GetSecretAsync($"{identifiyer}{secretName}/", new CancellationToken())).Returns(Task.FromResult(secretMock.Object));
var helper = new KeyVaultHelper(keyVaultClientMock.Object, "https://abc.vault.azure.net");
Assert.AreEqual("bar", await helper.GetSecretValueAsync("foo"));
}
}
}
3.1. オプション付きのメソッドは、オプションをつける
keyVaultClientMock.Setup(c => c.GetSecretAsync($"{identifiyer}{secretName}/", new CancellationToken())).Returns(Task.FromResult(secretMock.Object));
わたしは、GetSecretAsync のメソッドには一つしか引数を渡していませんが、ここでは、CancellationTokenを渡しています。Moq を使用する場合は、オプションの引数も渡してあげる必要があります。
3.2. Async メソッドの戻り値を Mock する場合は、 Task.FromResult を使う
通常、async / await のメソッドを書いていると、勝手に Task にくるんで返してくれるみたいですが、Moq を使っているときは、Task で返すことを意識する必要があります。ここでは、Task.FromResultに目標のオブジェクトをくるんで返しています。
3.3. Mock オブジェクトをコンストラクタは引数に渡す場合は Object を使う
Mock オブジェクトは、モック元のオブジェクトと型が異なるので、Object プロパティを呼んで、Mock 化したオブジェクトを渡すようにします。secretMock.Object
がそれに該当します。
3.4. await を忘れずに
Async Programming : Unit Testing Asynchronous Code を参考にしました。このテストでは、下記の部分で await をつけています。そうでないと、テストが終了する前に、メソッドが終了してしまうかもしれないからです。
Assert.AreEqual("bar", await helper.GetSecretValueAsync("foo"));
3.5. public async Task ... をメソッド名の前に
この部分ですが、public Task async TestGetSecret()
だと、[TestMethod]
が認識されなくなります。この順番で。
[TestMethod]
public async Task TestGetSecret()
実行、そして敗北
俺カッケー!完璧にモックしちゃったよ。しかも、コンパイルもできるしな。じゃあテストすっか。
ん、何よこのエラー!検索すっか。
[Invalid setup on a non-virtual (overridable in VB) member
c# - Invalid setup on a non-virtual (overridable in VB) member - Stack Overflow
ん、解決策はって、「メソッドに virtual つけなさい」
師匠の「金言」
詰んだ、完全に詰んだ。せっかくここまでやったのに、、、orz たまたまテストのためのクラス設計について師匠に問い合わせていた。詰んだので詰んだことを師匠に報告しようとしたらこんな答えが返ってきた。
KeyVaultHelperくらいのロジックだとテスト諦めると思いますねー
えー、確かに労力に割が合わない!
そのかわりKeyVaultHelperはインターフェース実装するようにして、こいつを使うクラスをテストするようにすると思います
あああああ!なるほど!!!師匠素晴らしすぎる、、、たしかに、このクラスを薄くして、Mock して、本体(ここでは、Azure Functions )をしっかりテストすればいいんだ。労力的にそっちのほうが絶対いいな。
というわけで、最終系。このクラスのテストはあきらめた。
using Microsoft.Azure.KeyVault;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.Threading.Tasks;
namespace KeyVault
{
public interface IKeyVaultHelper
{
Task<string> GetSecretValueAsync(string name);
}
public class KeyVaultHelper : IKeyVaultHelper
{
: 以下略
}
おわり。
テスト駆動クラスタの皆様へ
というわけで、私はこのクラスのテストはあきらめたわけですが、テスト駆動クラスタの皆様。もし、よりよい解決策やテストをうまく書く方法があれば是非コメントいただければと思います。