メインコンテンツまでスキップ

「依存関係逆転の原則(DIP)」タグの記事が2件件あります

全てのタグを見る

初級者向け!手書きMoqを使ってユニットテストをさらに進めよう(後編)

鶴田
ディアシステム(株)開発一部第2課

こんにちは。開発一部の鶴田です。

この記事は前編の続きです。前編ではゴールと大まかな実装方針、用語の解説を紹介しました。今回はその続きとして実際にコードを書いてユニットテストを実施します。

実装:製品マスター

まずは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);
}

ユニットテストを実行し、カバレッジを確認しましょう。 Fine Code Coverageの結果イメージ

よし、分岐までカバレッジ達成です。

実装:スパイを使ったテスト

では次にスパイを使って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);
}

ここまでできたら、ユニットテストを実行してカバレッジを確認しましょう。 Fine Code Coverageの結果イメージ

まとめ

今回はMoqを使わないという制約の中で、スタブとスパイを手書きしながらユニットテストを進めてみました。

  • インターフェイス+リポジトリで依存を切り離すと、実装を差し替えるだけでテスト環境を整えられる

  • スパイ+ Verifyを使うと、戻り値だけでは見落としがちな「振る舞い」も確認できる

  • OSSが使えないケースでも工夫次第で十分テストを強化できる

もし「まだ全部を取り入れる余裕がない」と感じても、まずはInMemoryリポジトリだけ作ってみるだけでも効果があります。小さな一歩を積み重ねながら、一緒にテストの質を向上させていきましょう!

※今回作ったプログラムは ここ からダウンロードできます。

初級者向け!手書きMoqを使ってユニットテストをさらに進めよう(前編)

鶴田
ディアシステム(株)開発一部第2課

こんにちは。開発2課の鶴田です。

この記事は、前回の「ユニットテストを使ってカバレッジを計測、テストの品質を向上させよう」の続編です。前回は、主に「プログラムのロジックが十分にテストされているか(=カバレッジ)」を確認することで、テストの質を高める方法を紹介しました。

今回はそこから一歩進んで、実際のアプリでよく登場する「データベースアクセス」や「外部データとのやりとり」が入った場面でも、ユニットテストを効果的に行う方法を紹介します。

一般的にはMoqというオープンソースソフトウェア(以下、OSSと呼びます)を使って、データベースの代わりとなる代用オブジェクトを作成、テストします。しかし、実際の業務では開発ルール上、OSSを利用できないケースもあります。

そこで今回は、OSSを使わずに実装する方法を紹介します。 OSSを使わずに手でMoq的なモノを実装することで、Moqの基本的な概念に対する理解が深まる効果もあります♪

いきなり実装に入ると混乱しやすいため、この記事(前編)では今回のゴールと実装方針、用語の解説を先に紹介します。次回の後編で、実際にコードを書いてみましょう。

※後編の実装編ではわたしが書いた記事を流用します。記事はこちらです。

今回のゴールと実装方針

実際のアプリでは、商品マスター(前回の記事ではProduct)や在庫マスター(前回の記事ではZaiko)の情報は「データベース」や「CSVファイル」などの外部データソースから取得されます。しかしそれをそのままユニットテスト化しようとすると、データベースの準備や接続が必要となり、ユニットテストを実装・実行するためのコストが上がります。

そこで今回は、データ取得の仕組みをインターフェイスをつかって抽象化し、本番環境とテスト環境で差し替えられるようにする方針を取ります。

たとえば、商品マスター(前回の記事ではProduct)の場合は以下の通りとなります。

public interface IProductRepository
{
// Find は習慣的に決まっている名前
Product? Find(string productCode);
}

// テスト用の取得・更新用クラスはsealedにすることが一般的
public sealed class InMemoryProductRepository : IProductRepository
{
// テスト用 Product 向けリポジトリクラス
Product? Find(string productCode)
{
// テスト用の値を返すロジック
return new Product



}
}

このように「データの取り出し方」だけを切り離すことで、DBやテスト用CSVがなくてもユニットテストを実行することができ、結果として保守しやすいコードになります。

用語の解説

Moq(モック)

Moqは .NET向けに広く使われているOSSです。インターフェイスや仮想メソッドに対して、簡単に「スタブ」や「スパイ」として振る舞うオブジェクトを生成できます。

var mock = new Mock<IProductRepository>();
mock.Setup(x => x.Find("001")).Returns(new Product("001", "商品A", 100));

このように記述することで、Find("001") の戻り値をテストごとに制御できます。また、呼び出し回数の確認(Verify)なども可能です。とても便利なライブラリですが、本記事では使用しない方針で進めます。

リポジトリ (Repository)

リポジトリとは「データの取得や保存のしくみを隠すためのクラス」です。 実装用とテスト用、2パターンを用意し、共通のインターフェイス経由で実装することで差し替えることができるようになります。

テストダブル (Test Double)

「テストのときに、本物の代わりに使うオブジェクト」の総称です。今回は以下の2つの機能をつかいます。

スタブ:決まった値を返すだけ。今回の例ではInMemoryProductRepositoryがスタブに該当します スパイ:呼び出されたかどうかを記録します。今回は呼び出された回数を内部で記録、検証します。

依存関係逆転の原則(DIP)と依存性の注入(DI)

テスト用のリポジトリに差し替えるためには、依存関係逆転の原則(DIP)と依存性の注入(DI)という考え方が重要になります。

依存関係逆転の原則(DIP) は以下の2つのルールからなりたっています。

  1. 上位のモジュールは下位のモジュールに依存してはならない。

  2. 抽象詳細に依存してはならない。

    • 上位 ・・・・ビジネスロジックやアプリ全体を制御するロジック
    • 下位 ・・・・DBやCSV、JSONに対する入出力ロジック
    • 依存する ・・ここでは実装を変更したときに影響を受けることを意味する。具体的な例でいうと、下位モジュールのコードを変更したときに、上位モジュールがエラーになったりすることを言う
    • 抽象 ・・・・インターフェィスや抽象クラスが該当する
    • 詳細 ・・・・具体的な実装

依存性の注入(DI) はオブジェクトが必要とする依存先(他のオブジェクトやサービス)を、外部から渡してもらうデザインパターンです。

ちなみにですが、私は「依存性の注入」がデザインパターンの名前のことであることに気づきませんでした。(名前って重要や・・・) 当社の土井がアップしたこちらの記事でわかりやすく解説しています。5分程度でサラッと読めるよい記事です。ご一読されることをおススメします。 依存関係逆転の原則とDI(Dependency Injection)

後編の予告

後編では、実際にInMemoryProductRepositoryやProductServiceを使ってユニットテストを書きます。 前回同様、Fine Code Coverageを使って、テストがしっかり書けているかも確認します。