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

「C#」タグの記事が3件件あります

全てのタグを見る

初級者向け!手書き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を使って、テストがしっかり書けているかも確認します。

初級者向け!ユニットテストを使ってカバレッジを計測、テストの品質を向上させよう

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

今回の記事では、ユニットテストを使ってカバレッジを計測し、さらに向上させる方法を解説します。

ユニットテストとは、プログラムの一部分を独立してテストする手法で、主に早期のバグ発見が目的です。 ユニットテストを導入すると、テストが十分に行き届いているかを確認できるようになり、テスト品質の 向上にも大いに役立ちます。さらに、リファクタリングや仕様変更の際に安心してコードを修正できるようになり、 開発効率の向上にも貢献するなど、開発全体において非常に重要な役割を果たします。

カバレッジとは、テスト対象のプログラムのどの部分が実際にテストされているかを示す指標です。 カバレッジを計測することで、テストが漏れている部分が明確になり、より高品質なテストへと つなげられます。

私自身、「ユニットテストでカバレッジを計測して品質を上げる」方法は、経験の浅いプログラマーにも わかりやすく効果的だと考えています。高品質な成果物はチーム全体の意識を高めるだけでなく、 モチベーションの向上にもつながります。 経験が浅い方でも、ユニットテストを活用すればチームに十分貢献できるでしょう。

「何から手を付ければいいかわからない」という方こそ、ぜひ参考にしてみてください。

動作環境

  • 開発環境: Visual Studio 2022 Community
  • 言語: C#

今回作成するプログラムの仕様

  • 製品クラス
    品番ごとに製品の名前と価格、廃番日付を保持する

  • 在庫クラス
    品番ごとに在庫数を保持する

  • 上記2クラスのユニットテストクラス

作成手順

  1. プロジェクトを2つ追加する

    • 1つ目のプロジェクト

      • プロジェクトの種類: クラスライブラリ
      • プロジェクトの名前: ClassLibrary1
    • 2つ目のプロジェクト

      • プロジェクトの種類: MSTestテストプロジェクト
      • プロジェクトの名前: TestProject1

    もともとある Class1.cs は削除してください。

  2. ClassLibrary1 に製品クラスと在庫クラスを追加する

製品マスタは「品番、製品名、価格、廃番日付」のプロパティを、在庫マスタは「品番、在庫数」のプロパティを持つようにします。下記のコードを追加してください。

製品クラス(Product)と在庫クラス(Zaiko)

public class Product
{
/// <summary>
/// 製品CD
/// </summary>
public string ProductCode { get; }

/// <summary>
/// 製品名称
/// </summary>
public string Name { get; }

/// <summary>
/// 価格
/// </summary>
public decimal Price { get; }

/// <summary>
/// 廃番日付
/// </summary>
public string? HaibanDate { get; }

public Product(string productCode, string name, decimal price)
{
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;
}
}

public class Zaiko
{
public string ProductCode { get; }
public int ZaikoSu { get; private set; }

public Zaiko(string productCode, int zaikoSu)
{
if (string.IsNullOrWhiteSpace(productCode)) throw new ArgumentException("製品CDは必須");
if (zaikoSu < 0) throw new ArgumentException("在庫数はゼロ以上のみ有効");

ProductCode = productCode;
ZaikoSu = zaikoSu;
}

public void AddZaikoSu(int amount)
{
if (amount <= 0) throw new ArgumentException("追加在庫数はゼロ以上のみ有効");
ZaikoSu += amount;
}
}

  1. TestProject1 にテストコードを追加する
[TestClass]
public class ProductTests
{
[TestMethod]
[Priority(0001)] // テストの優先度
public void コンストラクタ01()
{
var target = new Product("001", "商品A", 100, "20250101");
Assert.AreEqual("001", target.ProductCode);
Assert.AreEqual("商品A", target.Name);
Assert.AreEqual(100, target.Price);
Assert.AreEqual(target.HaibanDate, "20250101");
}
}
  1. 在庫クラスのテストコードを追加する
[TestClass]
public class ZaikoTests
{
[TestMethod]
[Priority(0001)]
public void コンストラクタ()
{
var target = new Zaiko("001", 10);
Assert.AreEqual("001", target.ProductCode);
Assert.AreEqual(10, target.ZaikoSu);
}
}

Visual Studio でテストを実行すると、テストが実行され、テスト結果が表示されます。

テストエクスプローラー実行結果

これで完成・・・でしょうか?

それを確認するために、カバレッジを計測してみましょう。

VisualStudio 2022 へ Fine Code Coverage を導入する

  1. カバレッジを計測するために、以下のツールを使用します。

coverlet.console NuGet パッケージマネージャーより、coverlet.console をインストールします。

Fine Code Coverage https://marketplace.visualstudio.com/items?itemName=FortuneNgwenya.FineCodeCoverage

拡張機能より、Fine Code Coverage を検索してインストールしてください。

Visual Studio を再起動すると、ツールバーに Fine Code Coverage が表示されます。

拡張機能→Fine Code Coverage→設定 より色の設定を確認してください。以下の設定がおすすめです。

Fine Code Coverageおすすめ

Fine Code Coverage についての詳しい解説は、以下のサイトも参考になります。

カバレッジ100%へ

先ほどまで作成したソリューションを再度開いて、テストを実行します。

在庫クラスを確認してみましょう。

カバレッジ結果

オレンジや赤色の背景色がコード内に表示されています。 これは、赤はテストが実行されていない行(C0カバレッジ未達成)、オレンジは分岐の一部がテストされていない行(C1カバレッジ未達成)を示しています。

この行が「テストの漏れ」です。放置してはいけません。テストコードを追加します。

テストコードを以下の通り追加してください。

[TestClass]
public class ProductTests
{
[TestMethod]
[Priority(0001)] // テストの優先度を設定
public void コンストラクタ()
{
var target = new Product("001", "商品A", 100, "20250101");
Assert.AreEqual("001", target.ProductCode);
Assert.AreEqual("商品A", target.Name);
Assert.AreEqual(100, target.Price);
Assert.AreEqual(target.HaibanDate, "20250101");
}

[TestMethod]
[Priority(0002)]
public void コンストラクタ_Exception_ProductCode()
{
Assert.ThrowsException<ArgumentException>(() => new Product("", "商品A", 100));
}
/*
* 以下の通りにも記述可能です。
* ただ、カバレッジツールの制約上最後の } が未テスト行として認識されます。
*
[Priority(0002)]
[ExpectedException(typeof(ArgumentException))] // 例外が発生することを確認するテストコード
public void コンストラクタ_Exception_ProductCode()
{
var target = new Product("", "商品A", 100);
}
*/

[TestMethod]
[Priority(0003)]
public void コンストラクタ_Exception_Name()
{
Assert.ThrowsException<ArgumentException>(() => new Product("001", "", 100));
}

[TestMethod]
[Priority(0004)]
public void コンストラクタ_Exception_Price()
{
Assert.ThrowsException<ArgumentException>(() => new Product("001", "商品A", -1M));
}

[TestMethod]
[Priority(0005)]
public void コンストラクタ_Exception_HaibanDate()
{
Assert.ThrowsException<ArgumentException>(() => new Product("001", "商品A", 100, "100"));
}
}

public class ZaikoTests
{
[TestMethod]
[Priority(0001)]
public void コンストラクタ()
{
var target = new Zaiko("001", 10);
Assert.AreEqual("001", target.ProductCode);
Assert.AreEqual(10, target.ZaikoSu);
}

[TestMethod]
[Priority(0002)]
public void コンストラクタ_Exception_ProductCode()
{
Assert.ThrowsException<ArgumentException>(() => new Zaiko("", 10));
}

[TestMethod]
[Priority(0003)]
public void コンストラクタ_Exception_ZaikoSu()
{
Assert.ThrowsException<ArgumentException>(() => new Zaiko("001", -1));
}

[TestMethod]
[Priority(0101)]
public void AddZaikoSu_Exception()
{
var target = new Zaiko("001", 10);
Assert.ThrowsException<ArgumentException>(() => target.AddZaikoSu(-1));
}

[TestMethod]
[Priority(0102)]
public void AddZaikoSu()
{
var target = new Zaiko("001", 10);
target.AddZaikoSu(1);
Assert.AreEqual(11, target.ZaikoSu);
}
}

これですべての行がテストされました。 Fine Code Coverage で確認してみましょう。

表示→その他のウィンドウ→Fine Code Coverage を選択します。

カバレッジ結果

確かにカバレッジの欄には燦然と輝く100%の文字がっ!

これで、テストの漏れがなくなりました。

もちろん、カバレッジを100%にしたからといって、バグが完全になくなるわけではありません。しかし、ユニットテストを書く過程でコードを深く理解できるようになり、結果として品質向上につながります。

ぜひ、ユニットテストを書いてカバレッジを活用してみてください。そうすることで、チーム全体の品質意識が高まり、開発効率も向上するはずです。