初級者向け!手書きMoqを使ってユニットテストをさらに進めよう(後編)
こんにちは。開発一部の鶴田です。
この記事は前編の続きです。前編ではゴールと大まかな実装方針、用語の解説を紹介しました。今回はその続きとして実際にコードを書いてユニットテストを実施します。
実装:製品マスター
まずはProduct:製品マスタークラスです。
public string ProductCode { get; }
public string Name { get; }
public decimal Price { get; }
public string? HaibanDate { get; }
public Product(string productCode, string name, decimal price, string? haibanDate = null)
{
if (string.IsNullOrWhiteSpace(productCode)) throw new ArgumentException("製品CDは必須です");
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("名称は必須です");
if (price < 0) throw new ArgumentException("製品価格は必須です");
if (haibanDate != null)
{
if (!DateTime.TryParseExact(haibanDate, "yyyyMMdd", null, System.Globalization.DateTimeStyles.None, out _))
throw new ArgumentException("廃番日が有効な日付ではない");
}
ProductCode = productCode;
Name = name;
Price = price;
HaibanDate = haibanDate;
}
実装:製品マスター向けリポジトリ
続いて追加するリポジトリです。リポジトリはプロジェクトフォルダー内にRepositoryフォルダーを作成することが一般的です。 リポジトリとして2つ、抽象化するためのインターフェイスとテスト用のスタブを登録します。
public interface IProductRepository
{
// 今回はFindのみ
Product? Find(string productCode);
}
InMemoryProductRepository
// sealedとする理由:「このクラスは継承しないでほしい」旨の意思表示
public sealed class InMemoryProductRepository : IProductRepository
{
private readonly Dictionary<string, Product> _store = new();
public void Add(Product product)
{
_store[product.ProductCode] = product;
}
public Product? Find(string productCode)
{
return _store.TryGetValue(productCode, out var product) ? product : null;
}
}
実装:製品マスター向けサービス
つづいてサービスクラスです。サービスクラスとは、ビジネスロジック(業務を表現したロジック)をまとめたクラスのことです。 製品コードをもとに製品名を返すシンプルなサービスクラスを用意します。 サービスクラスはプロジェクトフォルダー配下のServiceフォルダーに格納するのが一般的です。
public class ProductService
{
private readonly IProductRepository _repository;
public ProductService(IProductRepository repository)
{
_repository = repository;
}
public string? GetProductName(string productCode)
{
return _repository.Find(productCode)?.Name;
}
}
つい、「InMemoryProductRepositoryから直接取得すればいいんじゃね?」 と感じてしまうかもしれませんが・・・いや、それだと差し替えできないでしょ?笑
実装:製品マスター向けユニットテスト
そして、追加したロジックをテストするユニットテストを追加しましょう。
[TestMethod]
public void GetProductName_ValidCode_ReturnsName()
{
var repo = new InMemoryProductRepository();
repo.Add(new Product("001", "商品A", 100));
// ProductServiceオブジェクトが必要とする依存先を(repo)を外部から注入する
var service = new ProductService(repo);
var result = service.GetProductName("001");
Assert.AreEqual("商品A", result);
}
[TestMethod]
public void GetProductName_InvalidCode_ReturnsNull()
{
var repo = new InMemoryProductRepository();
// ProductServiceオブジェクトが必要とする依存先を(repo)を外部から注入する
var service = new ProductService(repo);
var result = service.GetProductName("999");
Assert.IsNull(result);
}
ユニットテストを実行し、カバレッジを確認しましょう。
よし、分岐までカバレッジ達成です。
実装:スパイを使ったテスト
では次にスパイを使ってVerifyをとりましょう。今回はサービスクラスからリポジトリクラスを呼び出された回数を確認します。
スパイを使うテストは、戻り値だけでは担保できないケースをテストする場合に行います。例を挙げると・・・・
- ビジネスロジックの呼び出し順が正しいか
- 重い処理を何度も呼び出していないか
- 不可逆的な処理のチェック(ログ出力、メール送信)
などのケースがあります。
ではスパイオブジェクト:SpyProductRepositoryを作成します。保存場所はテストプロジェクトの配下、TestDoubleに置くのが一般的です。
public class SpyProductRepository : IProductRepository
{
public int CallCount { get; private set; } = 0;
public Product? Find(string productCode)
{
// メソッドが呼ばれた回数をカウント
CallCount++;
return new Product(productCode, "Spy商品", 123);
}
}
今回はメソッドが呼ばれた回数をカウントしています。 ビジネスロジックの呼び出し順を検証するときは、文字列にメソッド名を追記して、それを検証します。 難しく考えることはないです。こんな感じです。そうです意外と泥臭いモンです笑
public Product? Find(string productCode)
{
// メソッドが呼ばれたときにCalledMethodへ追加
CalledMethod += @$" SpyProductRepository.Find( productCode:\"{productCode}\" )";
return new Product(productCode, "Spy商品", 123);
}
話を戻します。テストコードです。
[TestMethod]
public void GetProductName_RepositoryCalledOnce()
{
var repo = new SpyProductRepository();
// ProductServiceオブジェクトが必要とする依存先を(repo)を外部から注入する
var service = new ProductService(repo);
service.GetProductName("001");
Assert.AreEqual(1, repo.CallCount);
}
ここまでできたら、ユニットテストを実行してカバレッジを確認しましょう。
まとめ
今回はMoqを使わないという制約の中で、スタブとスパイを手書きしながらユニットテストを進めてみました。
-
インターフェイス+リポジトリで依存を切り離すと、実装を差し替えるだけでテスト環境を整えられる
-
スパイ+ Verifyを使うと、戻り値だけでは見落としがちな「振る舞い」も確認できる
-
OSSが使えないケースでも工夫次第で十分テストを強化できる
もし「まだ全部を取り入れる余裕がない」と感じても、まずはInMemoryリポジトリだけ作ってみるだけでも効果があります。小さな一歩を積み重ねながら、一緒にテストの質を向上させていきましょう!
※今回作ったプログラムは ここ からダウンロードできます。