C# Testing入門|xUnit・NUnit・MSTestで始める単体テストの書き方と実行手順

はじめに

C#でアプリケーションを開発していると、「修正したら別の機能が壊れていないか」「このメソッドは本当に想定どおり動いているか」を毎回手作業で確認するのは大変です。そこで役立つのが、C# Testing、つまりC#におけるテストコードの作成と自動実行です。

特に単体テストは、クラスやメソッドなど小さな単位の動作を確認するための基本的なテストです。C#では、xUnit、NUnit、MSTestといったテストフレームワークを使うことで、比較的簡単に単体テストを始められます。

この記事では、C# Testingの基本から、xUnit・NUnit・MSTestの違い、テストプロジェクトの作成方法、テストコードの書き方、実行手順、モックの使い方、よくあるエラーまでを初心者向けに解説します。これからC#で単体テストを学びたい方が、実際に手を動かして始められる内容を目指します。

1. C# Testingとは?検索ユーザーがまず知りたい単体テストの基本

C# Testingとは、C#で書かれたプログラムが期待どおりに動作するかを確認するために、テストコードを書いて検証することです。単にアプリケーションを起動して画面を触るだけでなく、メソッド単位、クラス単位、機能単位で自動的に確認できるようにするのが特徴です。

C#のテストは、.NETの開発環境と相性がよく、dotnet testコマンドやVisual Studioのテストエクスプローラーから簡単に実行できます。開発中の品質確認、リファクタリング時の安全性確保、CI/CDでの自動チェックなど、現場の開発プロセスでも広く使われます。

1-1. C# Testingの意味と.NETにおけるテストの位置づけ

C# Testingは、C#アプリケーションの動作をテストコードによって検証する取り組み全般を指します。.NETでは、テスト対象のプロジェクトとは別にテスト用プロジェクトを作成し、そこから本体プロジェクトを参照してテストを書く構成が一般的です。

たとえば、以下のような構成です。

MyApp/
MyApp.csproj
MyApp.Tests/
MyApp.Tests.csproj

MyAppが本体プロジェクト、MyApp.Testsがテストプロジェクトです。テストプロジェクトには、xUnit、NUnit、MSTestなどのテストフレームワークを導入し、テスト対象のクラスやメソッドを呼び出して結果を検証します。

.NETでは、テストは特別な追加作業ではなく、保守しやすいアプリケーションを作るための開発工程の一部です。特に業務システム、Web API、ライブラリ開発、長期運用されるアプリケーションでは、テストコードの有無が開発効率に大きく影響します。

1-2. 単体テスト・結合テスト・E2Eテストの違い

C# Testingを理解するうえで、まず押さえておきたいのがテストの種類です。代表的なものに、単体テスト、結合テスト、E2Eテストがあります。

単体テストは、1つのメソッドやクラスなど、小さな単位の動作を確認するテストです。たとえば、税込価格を計算するメソッド、入力値を検証するメソッド、文字列を整形するメソッドなどが対象になります。実行が速く、失敗したときに原因を特定しやすいのが特徴です。

結合テストは、複数のクラス、モジュール、外部サービスなどを組み合わせた状態で動作を確認するテストです。たとえば、サービスクラスがリポジトリを通じてデータベースにアクセスできるか、APIのリクエストからレスポンスまでが正しくつながるかを確認します。

E2Eテストは、ユーザー操作に近い形でシステム全体を確認するテストです。Webアプリケーションであれば、ブラウザを起動してログインし、画面操作を行い、最終的な表示やデータ更新を確認するようなテストが該当します。

単体テストは狭く速いテスト、E2Eテストは広く実際の利用に近いテストです。どれか1つだけで十分というより、目的に応じて組み合わせることが大切です。

1-3. C#で単体テストを書くメリット

C#で単体テストを書く最大のメリットは、コードの変更に強くなることです。機能追加や修正を行ったときに、既存の処理が壊れていないかを自動で確認できます。

また、単体テストを書くことで、処理の仕様が明確になります。たとえば「価格が0未満の場合は例外を投げる」「空文字の場合はfalseを返す」といった仕様をテストコードとして残せます。これは、将来の開発者にとってドキュメントのような役割も果たします。

さらに、テストしやすいコードを書こうとすると、自然と責務が分離された設計になりやすくなります。1つのクラスが多くのことをやりすぎているとテストが書きにくくなるため、単体テストは設計の悪さに気づくきっかけにもなります。

主なメリットは以下のとおりです。

・変更による不具合を早期に発見できる
・手動確認の手間を減らせる
・仕様をテストコードとして残せる
・リファクタリングしやすくなる
・設計の問題に気づきやすくなる
・CIで品質チェックを自動化できる

1-4. テストコードを書くべき処理・書かなくてもよい処理

すべてのコードに対して無理に単体テストを書く必要はありません。重要なのは、テストを書く価値が高い処理を見極めることです。

テストコードを書くべき処理としては、計算ロジック、条件分岐が多い処理、業務ルール、入力値検証、例外処理、日付や金額を扱う処理などがあります。これらはバグが発生すると影響が大きく、手動確認もしにくいため、単体テストの効果が高い領域です。

一方で、単純なプロパティのgetter/setterだけのクラスや、フレームワークの機能をそのまま呼び出しているだけの薄いコードは、単体テストの優先度が低い場合があります。たとえば、自動実装プロパティしか持たないDTOに対して細かくテストを書く必要はあまりありません。

ただし、「書かなくてもよい」というより、「優先度を下げてもよい」と考えるのが現実的です。限られた時間で効果を出すには、不具合が起きやすい処理や、変更頻度の高い処理からテストを書くのがおすすめです。

1-5. C# Testingを始める前に必要な前提知識

C# Testingを始める前に、最低限知っておきたい知識があります。まず、C#のクラス、メソッド、アクセス修飾子、例外処理、ジェネリクス、非同期処理の基本です。特に単体テストでは、テスト対象のメソッドを呼び出し、戻り値や例外を検証するため、メソッドの入出力を理解している必要があります。

次に、.NETのプロジェクト構成も重要です。.csprojファイル、プロジェクト参照、NuGetパッケージ、dotnet CLIの基本を知っておくと、テスト環境の構築がスムーズになります。

また、テストコードでは「期待値」と「実際の値」を比較します。そのため、テスト対象の仕様を明確にする力も必要です。何を入力したら何が返るべきか、どのような場合に例外を発生させるべきかを整理できると、テストコードを書きやすくなります。

2. C#の主要テストフレームワーク比較|xUnit・NUnit・MSTest

C#で単体テストを書くときによく使われるフレームワークが、xUnit、NUnit、MSTestです。どれも.NETのテストに対応しており、dotnet testやVisual Studioから実行できます。

初心者にとって悩みやすいのは、「どのテストフレームワークを選べばよいか」です。結論から言うと、新規プロジェクトではxUnitが選ばれることが多く、既存プロジェクトではすでに採用されているフレームワークに合わせるのが基本です。

それぞれの特徴を見ていきましょう。

2-1. xUnitとは?特徴と向いているプロジェクト

xUnitは、.NET向けのモダンなテストフレームワークです。シンプルな設計で、現在のC#プロジェクトでもよく使われています。

xUnitでは、通常のテストに[Fact]属性、パラメータ化テストに[Theory]属性を使います。テストクラスのコンストラクタが前処理、IDisposableDisposeメソッドが後処理として使えるため、C#らしい書き方ができるのも特徴です。

xUnitは、新規の.NETプロジェクト、ライブラリ開発、ASP.NET Coreアプリケーションのテストなどに向いています。シンプルで学習しやすく、dotnet new xunitで簡単にテストプロジェクトを作成できます。

C#
public class CalculatorTests
{
[Fact]
public void Add_ReturnsSum()
{
var result = 1 + 2;

Assert.Equal(3, result);
}
}

2-2. NUnitとは?特徴と向いているプロジェクト

NUnitは、歴史のある.NET向けテストフレームワークです。豊富な機能と柔軟な属性が特徴で、複雑なテストケースを扱いやすいフレームワークです。

NUnitでは、基本的なテストに[Test]属性、パラメータ化テストに[TestCase]属性を使います。また、Assert.Thatによる読みやすい検証構文もよく使われます。

C#
[Test]
public void Add_ReturnsSum()
{
var result = 1 + 2;

Assert.That(result, Is.EqualTo(3));
}

NUnitは、既存の.NET Frameworkプロジェクト、長く運用されている業務システム、複雑なテスト条件を扱うプロジェクトに向いています。テストケースの表現力が高いため、さまざまな入力パターンを検証したい場合にも便利です。

2-3. MSTestとは?特徴と向いているプロジェクト

MSTestは、Microsoftが提供するテストフレームワークです。Visual Studioとの親和性が高く、Microsoft標準のツールを使いたい場合に選択しやすいフレームワークです。

MSTestでは、テストクラスに[TestClass]、テストメソッドに[TestMethod]を付けます。パラメータ化テストには[DataTestMethod][DataRow]を使います。

C#
[TestClass]
public class CalculatorTests
{
[TestMethod]
public void Add_ReturnsSum()
{
var result = 1 + 2;

Assert.AreEqual(3, result);
}
}

MSTestは、Visual Studio中心の開発環境、Microsoft標準に寄せたいプロジェクト、既存でMSTestが使われているプロジェクトに向いています。

2-4. xUnit・NUnit・MSTestの違いを比較表で整理

xUnit、NUnit、MSTestはいずれもC# Testingで利用できますが、属性名や書き方、思想に違いがあります。

項目xUnitNUnitMSTest
基本テスト属性[Fact][Test][TestMethod]
パラメータ化テスト[Theory] + [InlineData][TestCase][DataTestMethod] + [DataRow]
テストクラス属性不要不要[TestClass]が必要
前処理コンストラクタ[SetUp][TestInitialize]
後処理Dispose[TearDown][TestCleanup]
特徴シンプルでモダン高機能で柔軟Microsoft標準
向いているケース新規.NET開発複雑なテストケースVisual Studio中心の開発

どのフレームワークでも、テストを書く目的は同じです。重要なのは、プロジェクト内でフレームワークを統一し、チームが理解しやすい形で運用することです。

2-5. 初心者はどのテストフレームワークを選ぶべきか

初心者がこれからC# Testingを始めるなら、まずはxUnitを選ぶのがおすすめです。理由は、書き方がシンプルで、.NET CLIからプロジェクトを作成しやすく、ASP.NET Coreなどの現代的な.NET開発でもよく使われるためです。

ただし、Visual Studioを中心に学びたい場合や、Microsoft公式の構成に合わせたい場合はMSTestでも問題ありません。既存の教材や社内プロジェクトがNUnitを使っているなら、NUnitを選ぶのも自然です。

初心者にとって大切なのは、最初からすべてのフレームワークを完璧に覚えることではありません。まずは1つを選び、テストの考え方、Assertの書き方、実行方法に慣れることが重要です。

2-6. 既存プロジェクトでフレームワークを選ぶ判断基準

既存プロジェクトにテストを追加する場合は、すでに使われているフレームワークに合わせるのが基本です。プロジェクト内にxUnitのテストがあるならxUnit、NUnitがあるならNUnit、MSTestがあるならMSTestを使うべきです。

複数のテストフレームワークを混在させると、パッケージ管理、実行設定、テストの書き方が複雑になります。特別な理由がない限り、統一したほうが保守しやすくなります。

フレームワークを新たに選ぶ場合は、以下の観点で判断します。

・チームメンバーが慣れているか
・既存のテストコードと統一できるか
・CI環境で問題なく実行できるか
・利用している.NETバージョンに対応しているか
・プロジェクトの規模やテスト方針に合っているか

3. C# Testingの開発環境を準備する

C# Testingを始めるには、.NET SDK、エディタまたはIDE、テストプロジェクト、テスト実行用パッケージが必要です。ここでは、dotnet CLIとVisual Studioの両方の観点から準備方法を説明します。

3-1. .NET SDKとVisual Studio/Visual Studio Codeの準備

まず、.NET SDKをインストールします。.NET SDKには、C#コードのビルド、実行、テストに必要なコマンドラインツールが含まれています。

インストール後、ターミナルで以下のコマンドを実行し、SDKが利用できるか確認します。

Bash
dotnet --version

バージョン番号が表示されれば準備できています。

開発環境としては、Visual StudioまたはVisual Studio Codeがよく使われます。Visual Studioはテストエクスプローラーが標準で使いやすく、GUIでテストを実行したい人に向いています。Visual Studio Codeは軽量で、dotnet CLIと組み合わせて使うのに向いています。

3-2. テスト対象プロジェクトとテストプロジェクトの構成

C#の単体テストでは、本体プロジェクトとテストプロジェクトを分けるのが一般的です。たとえば、以下のような構成にします。

SampleApp/
SampleApp.csproj
SampleApp.Tests/
SampleApp.Tests.csproj
SampleApp.sln

本体プロジェクトにはアプリケーションのコードを配置し、テストプロジェクトにはテストコードを配置します。テストプロジェクトから本体プロジェクトを参照することで、テスト対象のクラスやメソッドを呼び出せるようになります。

この構成にすると、本番コードとテストコードを明確に分離できます。また、テスト用パッケージが本体プロジェクトに混ざらないため、依存関係も整理しやすくなります。

3-3. dotnet CLIでテストプロジェクトを作成する方法

dotnet CLIを使うと、コマンドだけでテストプロジェクトを作成できます。以下は、xUnitのテストプロジェクトを作成する例です。

Bash
dotnet new sln -n SampleApp
dotnet new classlib -n SampleApp
dotnet new xunit -n SampleApp.Tests
dotnet sln add SampleApp/SampleApp.csproj
dotnet sln add SampleApp.Tests/SampleApp.Tests.csproj
dotnet add SampleApp.Tests/SampleApp.Tests.csproj reference SampleApp/SampleApp.csproj

これで、ソリューション、本体プロジェクト、テストプロジェクト、プロジェクト参照が作成されます。

NUnitの場合は以下のようにします。

Bash
dotnet new nunit -n SampleApp.Tests

MSTestの場合は以下です。

Bash
dotnet new mstest -n SampleApp.Tests

3-4. Visual Studioで単体テストプロジェクトを作成する方法

Visual Studioを使う場合は、GUIからテストプロジェクトを作成できます。

まず、ソリューションを開き、ソリューションエクスプローラーでソリューションを右クリックします。次に、「追加」から「新しいプロジェクト」を選択します。検索欄に「xUnit」「NUnit」「MSTest」などと入力し、目的のテストプロジェクトテンプレートを選びます。

作成後、テストプロジェクトから本体プロジェクトへの参照を追加します。ソリューションエクスプローラーでテストプロジェクトを右クリックし、「追加」から「プロジェクト参照」を選択して、本体プロジェクトにチェックを入れます。

Visual Studioでは、テストエクスプローラーを使うことで、テストの一覧表示、実行、失敗結果の確認ができます。

3-5. テストプロジェクトから本体プロジェクトを参照する方法

テストプロジェクトから本体プロジェクトを参照するには、dotnet add referenceコマンドを使います。

Bash
dotnet add SampleApp.Tests/SampleApp.Tests.csproj reference SampleApp/SampleApp.csproj

この参照を追加すると、テストプロジェクト側から本体プロジェクトのpublicなクラスやメソッドを呼び出せるようになります。

たとえば、本体プロジェクトに以下のクラスがあるとします。

C#
namespace SampleApp;

public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
}

テストプロジェクトでは、以下のように使用できます。

C#
using SampleApp;

public class CalculatorTests
{
[Fact]
public void Add_ReturnsSum()
{
var calculator = new Calculator();

var result = calculator.Add(1, 2);

Assert.Equal(3, result);
}
}

3-6. テスト実行に必要なNuGetパッケージ

テストプロジェクトには、テストフレームワーク本体と、テストを実行するためのパッケージが必要です。テンプレートから作成した場合、多くは自動で追加されます。

xUnitでは、主に以下のようなパッケージが使われます。

xunit
xunit.runner.visualstudio
Microsoft.NET.Test.Sdk

NUnitでは、以下のようなパッケージが使われます。

NUnit
NUnit3TestAdapter
Microsoft.NET.Test.Sdk

MSTestでは、以下のようなパッケージが使われます。

MSTest.TestFramework
MSTest.TestAdapter
Microsoft.NET.Test.Sdk

Microsoft.NET.Test.Sdkは、.NETでテストを検出・実行するために必要です。テストが検出されない場合、このパッケージやアダプターが不足していることがあります。

4. xUnitで始めるC#単体テストの書き方

xUnitは、C# Testingを始めるうえで扱いやすいテストフレームワークです。ここでは、xUnitのテストプロジェクト作成から、基本的なテスト、パラメータ化テスト、Assertの使い方までを解説します。

4-1. xUnitテストプロジェクトの作成手順

xUnitのテストプロジェクトは、以下のコマンドで作成できます。

Bash
dotnet new xunit -n SampleApp.Tests

既存のソリューションに追加する場合は、以下のようにします。

Bash
dotnet sln add SampleApp.Tests/SampleApp.Tests.csproj
dotnet add SampleApp.Tests/SampleApp.Tests.csproj reference SampleApp/SampleApp.csproj

作成直後のテストプロジェクトには、サンプルのテストファイルが含まれていることがあります。不要であれば削除し、自分のテスト対象に合わせたテストクラスを作成します。

4-2. Fact属性を使った基本的なテスト

xUnitで引数を取らない基本的なテストを書く場合は、[Fact]属性を使います。

C#
using SampleApp;
using Xunit;

namespace SampleApp.Tests;

public class CalculatorTests
{
[Fact]
public void Add_1And2_Returns3()
{
var calculator = new Calculator();

var result = calculator.Add(1, 2);

Assert.Equal(3, result);
}
}

[Fact]が付いたメソッドは、テストメソッドとして認識されます。テストメソッドはpublicである必要はありませんが、読みやすさのためにプロジェクトの方針に合わせて統一するとよいでしょう。

テスト名は、何を検証しているのかがわかる名前にします。Add_1And2_Returns3という名前であれば、「Addメソッドに1と2を渡すと3を返す」という意図が伝わります。

4-3. Theory属性とInlineDataを使ったパラメータ化テスト

同じロジックを複数の入力値で検証したい場合は、[Theory][InlineData]を使います。

C#
public class CalculatorTests
{
[Theory]
[InlineData(1, 2, 3)]
[InlineData(0, 0, 0)]
[InlineData(-1, 1, 0)]
public void Add_ReturnsExpectedSum(int a, int b, int expected)
{
var calculator = new Calculator();

var result = calculator.Add(a, b);

Assert.Equal(expected, result);
}
}

このテストは、InlineDataの行数分だけ実行されます。入力値と期待値をまとめて書けるため、境界値や複数パターンを検証するのに便利です。

パラメータ化テストを使うと、似たようなテストメソッドを何度も書かずに済みます。ただし、1つのテストに多くのパターンを詰め込みすぎると、失敗時に原因がわかりにくくなることがあります。意味の近いケースごとに分けると読みやすくなります。

4-4. Assert.Equal・Assert.True・Assert.Throwsの使い方

xUnitでは、Assertクラスを使って結果を検証します。よく使うのが、Assert.EqualAssert.TrueAssert.Throwsです。

Assert.Equalは、期待値と実際の値が等しいことを検証します。

C#
Assert.Equal(3, result);

Assert.Trueは、条件がtrueであることを検証します。

C#
Assert.True(user.IsActive);

ただし、単にAssert.True(result == 3)と書くより、Assert.Equal(3, result)と書いたほうが、失敗時に期待値と実際の値がわかりやすくなります。

Assert.Throwsは、例外が発生することを検証します。

C#
Assert.Throws<ArgumentException>(() =>
{
calculator.Divide(10, 0);
});

例外の内容まで確認したい場合は、戻り値として例外オブジェクトを受け取ります。

C#
var exception = Assert.Throws<ArgumentException>(() =>
{
calculator.Divide(10, 0);
});

Assert.Equal("除数に0は指定できません。", exception.Message);

4-5. Arrange-Act-Assertパターンで読みやすく書く

単体テストは、Arrange、Act、Assertの3つに分けて書くと読みやすくなります。

Arrangeは準備、Actは実行、Assertは検証です。

C#
[Fact]
public void Add_1And2_Returns3()
{
// Arrange
var calculator = new Calculator();

// Act
var result = calculator.Add(1, 2);

// Assert
Assert.Equal(3, result);
}

この形にすると、テストの意図が明確になります。テストが複雑になってきたときも、どこで準備し、どこで対象メソッドを呼び出し、どこで結果を検証しているかがわかりやすくなります。

コメントを毎回書く必要はありませんが、少なくとも構造としてArrange、Act、Assertを意識すると、保守しやすいテストコードになります。

4-6. xUnitのテストを実行する手順

xUnitのテストは、dotnet testで実行できます。ソリューションファイルがあるディレクトリで以下を実行します。

Bash
dotnet test

特定のテストプロジェクトだけを実行する場合は、プロジェクトファイルを指定します。

Bash
dotnet test SampleApp.Tests/SampleApp.Tests.csproj

Visual Studioを使っている場合は、テストエクスプローラーから実行できます。テストエクスプローラーにテストが表示されない場合は、ビルドエラーがないか、必要なNuGetパッケージが入っているかを確認しましょう。

5. NUnitで始めるC#単体テストの書き方

NUnitは、豊富な機能と読みやすいAssert構文を持つテストフレームワークです。ここでは、NUnitで単体テストを書く基本を解説します。

5-1. NUnitテストプロジェクトの作成手順

NUnitのテストプロジェクトは、以下のコマンドで作成できます。

Bash
dotnet new nunit -n SampleApp.Tests

ソリューションに追加し、本体プロジェクトを参照します。

Bash
dotnet sln add SampleApp.Tests/SampleApp.Tests.csproj
dotnet add SampleApp.Tests/SampleApp.Tests.csproj reference SampleApp/SampleApp.csproj

テンプレートで作成した場合、NUnit本体、NUnit3TestAdapter、Microsoft.NET.Test.Sdkなどが追加されます。

5-2. Test属性を使った基本的なテスト

NUnitで基本的なテストを書く場合は、[Test]属性を使います。

C#
using NUnit.Framework;
using SampleApp;

namespace SampleApp.Tests;

public class CalculatorTests
{
[Test]
public void Add_1And2_Returns3()
{
var calculator = new Calculator();

var result = calculator.Add(1, 2);

Assert.That(result, Is.EqualTo(3));
}
}

[Test]が付いたメソッドは、NUnitのテストとして認識されます。NUnitでは、Assert.AreEqualのような書き方もできますが、近年はAssert.Thatを使った制約ベースの書き方がよく使われます。

5-3. TestCase属性を使ったパラメータ化テスト

NUnitで複数の入力パターンを検証する場合は、[TestCase]属性を使います。

C#
public class CalculatorTests
{
[TestCase(1, 2, 3)]
[TestCase(0, 0, 0)]
[TestCase(-1, 1, 0)]
public void Add_ReturnsExpectedSum(int a, int b, int expected)
{
var calculator = new Calculator();

var result = calculator.Add(a, b);

Assert.That(result, Is.EqualTo(expected));
}
}

TestCaseを使うと、1つのテストメソッドで複数のケースを簡潔に表現できます。入力値と期待値を同じ行に書けるため、テストデータの見通しがよくなります。

5-4. Assert.Thatを使った検証の書き方

NUnitのAssert.Thatは、実際の値と条件を組み合わせて検証します。

C#
Assert.That(result, Is.EqualTo(3));
Assert.That(name, Is.Not.Empty);
Assert.That(items.Count, Is.GreaterThan(0));
Assert.That(user.IsActive, Is.True);

Assert.Thatの利点は、検証内容が自然な文章のように読めることです。たとえば、Assert.That(result, Is.EqualTo(3))は「resultが3と等しいこと」を表しています。

例外を検証する場合は、以下のように書けます。

C#
Assert.Throws<ArgumentException>(() =>
{
calculator.Divide(10, 0);
});

また、例外メッセージまで確認する場合は、例外を受け取って検証します。

C#
var exception = Assert.Throws<ArgumentException>(() =>
{
calculator.Divide(10, 0);
});

Assert.That(exception!.Message, Is.EqualTo("除数に0は指定できません。"));

5-5. SetUp・TearDownで前処理と後処理を共通化する

NUnitでは、各テストの前に実行したい処理を[SetUp]、各テストの後に実行したい処理を[TearDown]で定義できます。

C#
public class CalculatorTests
{
private Calculator _calculator = null!;

[SetUp]
public void SetUp()
{
_calculator = new Calculator();
}

[TearDown]
public void TearDown()
{
// 必要な後処理を書く
}

[Test]
public void Add_1And2_Returns3()
{
var result = _calculator.Add(1, 2);

Assert.That(result, Is.EqualTo(3));
}
}

SetUpを使うと、各テストで共通するインスタンス生成などをまとめられます。ただし、共通化しすぎるとテストごとの前提条件が見えにくくなることがあります。読みやすさを優先し、必要な範囲で使いましょう。

5-6. NUnitのテストを実行する手順

NUnitのテストも、dotnet testで実行できます。

Bash
dotnet test

特定のテストプロジェクトだけを実行する場合は、以下のように指定します。

Bash
dotnet test SampleApp.Tests/SampleApp.Tests.csproj

Visual Studioでは、NUnit3TestAdapterが入っていればテストエクスプローラーに表示されます。表示されない場合は、NuGetパッケージ、ビルドエラー、テストメソッドの属性を確認しましょう。

6. MSTestで始めるC#単体テストの書き方

MSTestは、Microsoftが提供するテストフレームワークです。Visual Studioとの相性がよく、標準的な構成でC# Testingを始めたい場合に使いやすい選択肢です。

6-1. MSTestテストプロジェクトの作成手順

MSTestのテストプロジェクトは、以下のコマンドで作成できます。

Bash
dotnet new mstest -n SampleApp.Tests

ソリューションに追加し、本体プロジェクトを参照します。

Bash
dotnet sln add SampleApp.Tests/SampleApp.Tests.csproj
dotnet add SampleApp.Tests/SampleApp.Tests.csproj reference SampleApp/SampleApp.csproj

MSTestでは、テストクラスに[TestClass]、テストメソッドに[TestMethod]を付けるのが基本です。

6-2. TestMethod属性を使った基本的なテスト

MSTestで基本的なテストを書く例です。

C#
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SampleApp;

namespace SampleApp.Tests;

[TestClass]
public class CalculatorTests
{
[TestMethod]
public void Add_1And2_Returns3()
{
var calculator = new Calculator();

var result = calculator.Add(1, 2);

Assert.AreEqual(3, result);
}
}

[TestClass]が付いたクラスの中にある[TestMethod]付きメソッドがテストとして認識されます。xUnitやNUnitと比べると属性がやや多いですが、構造が明確でわかりやすいのが特徴です。

6-3. DataTestMethodとDataRowを使ったパラメータ化テスト

MSTestでパラメータ化テストを書く場合は、[DataTestMethod][DataRow]を使います。

C#
[TestClass]
public class CalculatorTests
{
[DataTestMethod]
[DataRow(1, 2, 3)]
[DataRow(0, 0, 0)]
[DataRow(-1, 1, 0)]
public void Add_ReturnsExpectedSum(int a, int b, int expected)
{
var calculator = new Calculator();

var result = calculator.Add(a, b);

Assert.AreEqual(expected, result);
}
}

DataRowに指定した値が、テストメソッドの引数に渡されます。複数パターンの検証を簡潔に書けるため、計算処理や入力値検証のテストに向いています。

6-4. Assert.AreEqual・Assert.IsTrue・Assert.ThrowsExceptionの使い方

MSTestでは、Assertクラスを使って結果を検証します。

値が等しいことを確認するには、Assert.AreEqualを使います。

C#
Assert.AreEqual(3, result);

条件がtrueであることを確認するには、Assert.IsTrueを使います。

C#
Assert.IsTrue(user.IsActive);

例外が発生することを確認するには、Assert.ThrowsExceptionを使います。

C#
Assert.ThrowsException<ArgumentException>(() =>
{
calculator.Divide(10, 0);
});

例外メッセージまで確認したい場合は、例外オブジェクトを受け取ります。

C#
var exception = Assert.ThrowsException<ArgumentException>(() =>
{
calculator.Divide(10, 0);
});

Assert.AreEqual("除数に0は指定できません。", exception.Message);

6-5. TestInitialize・TestCleanupで前処理と後処理を共通化する

MSTestでは、各テストの前に実行する処理を[TestInitialize]、各テストの後に実行する処理を[TestCleanup]で定義できます。

C#
[TestClass]
public class CalculatorTests
{
private Calculator _calculator = null!;

[TestInitialize]
public void TestInitialize()
{
_calculator = new Calculator();
}

[TestCleanup]
public void TestCleanup()
{
// 必要な後処理を書く
}

[TestMethod]
public void Add_1And2_Returns3()
{
var result = _calculator.Add(1, 2);

Assert.AreEqual(3, result);
}
}

前処理を共通化すると重複を減らせますが、テストの前提条件が隠れすぎないように注意しましょう。シンプルなテストであれば、各テストメソッド内に準備処理を書いたほうが読みやすい場合もあります。

6-6. MSTestのテストを実行する手順

MSTestのテストも、dotnet testで実行できます。

Bash
dotnet test

特定のテストプロジェクトだけを実行する場合は、以下のようにします。

Bash
dotnet test SampleApp.Tests/SampleApp.Tests.csproj

Visual Studioでは、テストエクスプローラーからテストを実行できます。MSTestはVisual Studioとの親和性が高いため、GUIで実行結果を確認したい場合にも使いやすいです。

7. C# Testingの実践例|簡単なクラスをテストしてみる

ここからは、実際に簡単なクラスを用意して、正常系、異常系、境界値、例外のテストを書いてみます。テストフレームワークはxUnitを例にしますが、考え方はNUnitやMSTestでも同じです。

7-1. テスト対象となるサンプルクラスを作成する

まず、テスト対象となるPriceCalculatorクラスを作成します。

C#
namespace SampleApp;

public class PriceCalculator
{
public int CalculateTotal(int unitPrice, int quantity)
{
if (unitPrice < 0)
{
throw new ArgumentException("単価は0以上で指定してください。", nameof(unitPrice));
}

if (quantity < 0)
{
throw new ArgumentException("数量は0以上で指定してください。", nameof(quantity));
}

return unitPrice * quantity;
}

public bool IsFreeShipping(int totalPrice)
{
if (totalPrice < 0)
{
throw new ArgumentException("合計金額は0以上で指定してください。", nameof(totalPrice));
}

return totalPrice >= 5000;
}
}

このクラスには、合計金額を計算するCalculateTotalメソッドと、送料無料かどうかを判定するIsFreeShippingメソッドがあります。

7-2. 正常系のテストを書く

正常系とは、通常想定される入力に対して、期待どおりの結果が返ることを確認するテストです。

C#
using SampleApp;
using Xunit;

namespace SampleApp.Tests;

public class PriceCalculatorTests
{
[Fact]
public void CalculateTotal_UnitPriceAndQuantity_ReturnsTotalPrice()
{
var calculator = new PriceCalculator();

var result = calculator.CalculateTotal(1000, 3);

Assert.Equal(3000, result);
}
}

このテストでは、単価1000円、数量3の場合に、合計金額が3000円になることを確認しています。

正常系のテストは、もっとも基本的なテストです。まずは「普通に使ったときに正しく動くか」を確認しましょう。

7-3. 異常系のテストを書く

異常系とは、不正な入力や想定外の条件に対して、適切に処理できるかを確認するテストです。

たとえば、単価にマイナス値を指定した場合は例外を発生させる仕様にしています。

C#
[Fact]
public void CalculateTotal_NegativeUnitPrice_ThrowsArgumentException()
{
var calculator = new PriceCalculator();

var exception = Assert.Throws<ArgumentException>(() =>
{
calculator.CalculateTotal(-1, 3);
});

Assert.Contains("単価は0以上で指定してください。", exception.Message);
}

異常系のテストを書くことで、不正な入力が来たときの動作を明確にできます。業務アプリケーションでは、入力値検証や例外処理のテストは特に重要です。

7-4. 境界値のテストを書く

境界値とは、条件が切り替わる境目の値です。バグは境界値で発生しやすいため、単体テストでは意識して確認する必要があります。

IsFreeShippingメソッドでは、合計金額が5000円以上なら送料無料としています。この場合、境界値は4999円、5000円、5001円です。

C#
[Theory]
[InlineData(4999, false)]
[InlineData(5000, true)]
[InlineData(5001, true)]
public void IsFreeShipping_TotalPrice_ReturnsExpectedResult(int totalPrice, bool expected)
{
var calculator = new PriceCalculator();

var result = calculator.IsFreeShipping(totalPrice);

Assert.Equal(expected, result);
}

境界値をテストすると、「以上」「より大きい」「以下」「未満」の条件ミスを発見しやすくなります。特に金額、年齢、日付、件数、在庫数などの判定では境界値テストが重要です。

7-5. 例外が発生する処理をテストする

例外のテストでは、期待した例外型が発生するかを確認します。xUnitではAssert.Throwsを使います。

C#
[Fact]
public void IsFreeShipping_NegativeTotalPrice_ThrowsArgumentException()
{
var calculator = new PriceCalculator();

var exception = Assert.Throws<ArgumentException>(() =>
{
calculator.IsFreeShipping(-1);
});

Assert.Equal("totalPrice", exception.ParamName);
}

このテストでは、合計金額にマイナス値を指定したときにArgumentExceptionが発生し、対象の引数名がtotalPriceであることを確認しています。

例外テストを書くときは、単に「何か例外が出る」ではなく、「期待した種類の例外が出る」ことを確認するのがポイントです。

7-6. テストが失敗したときの原因を読み解く

テストが失敗した場合、まず確認するのは失敗メッセージです。多くの場合、期待値と実際の値が表示されます。

たとえば、以下のような失敗があったとします。

Assert.Equal() Failure
Expected: 5000
Actual: 4999

この場合、テストは5000を期待していましたが、実際には4999が返っています。原因としては、テスト対象コードの計算ミス、テスト側の期待値ミス、入力データの指定ミスが考えられます。

例外のテストで失敗した場合は、期待した例外型と実際に発生した例外型を確認します。例外が発生しなかった場合は、入力値が本当に異常値になっているか、テスト対象メソッドが呼ばれているかを確認しましょう。

8. dotnet testとVisual StudioでC#テストを実行する方法

C# Testingでは、テストを書くだけでなく、継続的に実行することが重要です。ここでは、dotnet testとVisual Studioを使ったテスト実行方法を解説します。

8-1. dotnet testコマンドの基本

dotnet testは、.NETのテストを実行する基本コマンドです。ソリューションファイルがあるディレクトリで実行すると、ソリューション内のテストプロジェクトが実行されます。

Bash
dotnet test

特定のプロジェクトだけを実行する場合は、テストプロジェクトを指定します。

Bash
dotnet test SampleApp.Tests/SampleApp.Tests.csproj

ビルドせずにテストだけを実行したい場合は、事前にビルド済みであることを前提に以下のようにできます。

Bash
dotnet test --no-build

ただし、コードを変更した直後はビルドも必要になるため、初心者のうちは通常のdotnet testを使うのがおすすめです。

8-2. 特定のテストだけを実行する方法

特定のテストだけを実行したい場合は、--filterオプションを使います。

Bash
dotnet test --filter "FullyQualifiedName~CalculatorTests"

特定のテストメソッド名で絞り込むこともできます。

Bash
dotnet test --filter "FullyQualifiedName~Add_1And2_Returns3"

テスト数が増えてくると、毎回すべてのテストを実行するのに時間がかかることがあります。開発中は関係するテストだけを実行し、コミット前やプルリクエスト前に全体を実行すると効率的です。

8-3. Visual Studioのテストエクスプローラーで実行する方法

Visual Studioでは、テストエクスプローラーからテストを実行できます。メニューから「テスト」→「テスト エクスプローラー」を開くと、検出されたテストが一覧表示されます。

テストエクスプローラーでは、すべてのテストを実行したり、選択したテストだけを実行したりできます。失敗したテストをクリックすると、失敗理由やスタックトレースを確認できます。

GUIで結果を確認できるため、初心者にはVisual Studioのテストエクスプローラーもおすすめです。一方、CI環境や自動化ではdotnet testを使うことが多いです。

8-4. テスト結果の見方

テスト結果では、主に以下を確認します。

・成功したテスト数
・失敗したテスト数
・スキップされたテスト数
・失敗したテスト名
・期待値と実際の値
・例外メッセージ
・スタックトレース

成功した場合は、すべてのテストが緑色で表示されます。失敗した場合は、どのテストが失敗したか、どのAssertで失敗したかを確認します。

スタックトレースには、失敗が発生したファイル名や行番号が表示されます。まずはテストコードの該当行を確認し、期待値と実際の値の差を見ましょう。

8-5. テスト失敗時に確認すべきポイント

テストが失敗したときは、すぐに本体コードだけを疑うのではなく、テストコードも確認します。テストの期待値が間違っている場合もあるためです。

確認すべきポイントは以下です。

・入力値は正しいか
・期待値は仕様と一致しているか
・テスト対象のメソッドを正しく呼び出しているか
・例外型は正しいか
・前処理で必要なデータが準備されているか
・テスト同士が状態を共有していないか
・ローカル環境に依存していないか

単体テストは、失敗したときに原因を特定しやすいことが重要です。1つのテストに多くの検証を詰め込みすぎると、どの振る舞いが壊れたのかわかりにくくなるため注意しましょう。

8-6. CI環境で自動テストを実行する基本

CI環境では、プッシュやプルリクエストをきっかけに自動でテストを実行します。GitHub Actions、Azure Pipelines、GitLab CIなどでdotnet testを実行する構成が一般的です。

GitHub Actionsの簡単な例です。

YAML
name: .NET Test

on:
push:
pull_request:

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'

- name: Restore
run: dotnet restore

- name: Test
run: dotnet test --no-restore

CIでテストを自動化すると、チーム開発で不具合の混入を早期に検出できます。ローカルでは動いていたのにCIで失敗する場合は、OS差異、環境変数、タイムゾーン、ファイルパス、外部サービス依存などを確認します。

9. モックを使ったC# Testing入門

単体テストでは、テスト対象のクラスだけを検証したい場面があります。しかし、実際のコードではデータベース、外部API、ファイル、現在時刻、メール送信などに依存していることがあります。こうした外部依存を切り離すために使うのがモックです。

9-1. モック・スタブ・フェイクの違い

モック、スタブ、フェイクは、テスト用の代替オブジェクトを表す言葉です。厳密には意味が異なります。

スタブは、テストに必要な値を返すだけの代替オブジェクトです。たとえば、常に固定のユーザー情報を返すリポジトリなどが該当します。

モックは、呼び出し内容を検証するための代替オブジェクトです。たとえば、「メール送信メソッドが1回呼ばれたか」を確認する場合に使います。

フェイクは、簡易的に動作する実装です。たとえば、本物のデータベースの代わりにメモリ上のリストで動くリポジトリなどです。

現場ではこれらをまとめて「モック」と呼ぶこともありますが、目的を理解して使い分けるとテスト設計がわかりやすくなります。

9-2. 外部依存を分離すべき理由

単体テストでは、テスト対象のロジックだけを確認したいので、外部依存はできるだけ分離します。データベースや外部APIに直接アクセスするテストは、実行が遅く、環境に依存し、失敗原因の特定が難しくなります。

たとえば、ユーザー登録時にメールを送る処理をテストする場合、本当にメールを送信する必要はありません。確認したいのは、「条件を満たしたときにメール送信処理が呼ばれるか」です。

外部依存をインターフェースとして切り出し、テスト時にはモックに差し替えることで、単体テストを速く安定させることができます。

9-3. Moqを使ったモックの基本

C#でよく使われるモックライブラリの1つにMoqがあります。Moqを使うと、インターフェースのモックを簡単に作成できます。

まず、メール送信用のインターフェースを用意します。

C#
public interface IEmailSender
{
void Send(string to, string subject, string body);
}

このインターフェースを使うサービスクラスを作成します。

C#
public class UserService
{
private readonly IEmailSender _emailSender;

public UserService(IEmailSender emailSender)
{
_emailSender = emailSender;
}

public void Register(string email)
{
if (string.IsNullOrWhiteSpace(email))
{
throw new ArgumentException("メールアドレスは必須です。", nameof(email));
}

_emailSender.Send(email, "登録完了", "登録ありがとうございます。");
}
}

Moqを使ったテストは以下のように書けます。

C#
using Moq;
using Xunit;

public class UserServiceTests
{
[Fact]
public void Register_ValidEmail_SendsEmail()
{
var emailSenderMock = new Mock<IEmailSender>();
var service = new UserService(emailSenderMock.Object);

service.Register("test@example.com");

emailSenderMock.Verify(
x => x.Send("test@example.com", "登録完了", "登録ありがとうございます。"),
Times.Once);
}
}

このテストでは、本当にメールを送信せず、Sendメソッドが期待どおり1回呼ばれたことを検証しています。

9-4. インターフェースを使ってテストしやすい設計にする

モックを使うためには、外部依存を差し替えられる設計にする必要があります。そのためによく使われるのがインターフェースと依存性注入です。

悪い例として、クラス内で直接外部依存を生成している場合を考えます。

C#
public class UserService
{
public void Register(string email)
{
var sender = new SmtpEmailSender();
sender.Send(email, "登録完了", "登録ありがとうございます。");
}
}

この書き方だと、テスト時にSmtpEmailSenderを差し替えにくくなります。

一方、コンストラクタでインターフェースを受け取るようにすると、テスト時にモックを渡せます。

C#
public class UserService
{
private readonly IEmailSender _emailSender;

public UserService(IEmailSender emailSender)
{
_emailSender = emailSender;
}
}

このように、テストしやすい設計は、変更しやすい設計にもつながります。

9-5. データベース・API・ファイル操作を直接テストしない考え方

単体テストでは、データベース、外部API、ファイル操作を直接扱わないのが基本です。これらは実行環境に依存しやすく、テストが不安定になりやすいためです。

たとえば、データベースに接続する処理を単体テストで毎回実行すると、テストデータの準備、接続文字列、実行順序、データの後片付けが問題になります。外部APIであれば、ネットワーク状態やAPI側の障害によってテストが失敗することもあります。

単体テストでは、データベースやAPIを呼び出す部分をインターフェースにし、モックやフェイクに置き換えます。本当にデータベースとの接続を確認したい場合は、単体テストではなく結合テストとして扱うのが自然です。

9-6. モックを使いすぎないための注意点

モックは便利ですが、使いすぎるとテストが実装詳細に依存しすぎることがあります。たとえば、「このメソッドがこの順番で呼ばれること」ばかりを検証していると、内部実装を少し変更しただけでテストが壊れます。

単体テストで本当に確認したいのは、外から見た振る舞いです。戻り値、状態の変化、例外、重要な外部呼び出しなどを中心に検証しましょう。

モックを使うべき場面は、外部依存を切り離したいとき、呼び出しを検証したいとき、実行コストが高い処理を避けたいときです。単純な値オブジェクトや計算ロジックまで無理にモック化する必要はありません。

10. C#のテストコードを読みやすく保つベストプラクティス

テストコードは、本体コードと同じくらい保守が必要です。読みにくいテスト、壊れやすいテスト、意図がわからないテストは、開発の助けになるどころか負担になります。

ここでは、C# Testingで意識したいテストコードのベストプラクティスを紹介します。

10-1. テスト名の付け方

テスト名は、何を検証しているかがわかる名前にします。よく使われる形式は以下です。

メソッド名_条件_期待結果

たとえば、以下のような名前です。

C#
CalculateTotal_ValidInput_ReturnsTotalPrice
CalculateTotal_NegativeUnitPrice_ThrowsArgumentException
IsFreeShipping_TotalPriceIs5000_ReturnsTrue

日本語のテスト名を使うチームもありますが、C#では英語のメソッド名にすることが多いです。大切なのは、チーム内で命名ルールを統一し、テスト名を見ただけで意図が伝わるようにすることです。

10-2. 1つのテストで1つの振る舞いを検証する

1つのテストでは、できるだけ1つの振る舞いを検証します。複数の振る舞いをまとめて検証すると、失敗したときに原因がわかりにくくなります。

悪い例です。

C#
[Fact]
public void Calculator_WorksCorrectly()
{
var calculator = new Calculator();

Assert.Equal(3, calculator.Add(1, 2));
Assert.Equal(2, calculator.Subtract(5, 3));
Assert.Equal(6, calculator.Multiply(2, 3));
}

このテストは、Add、Subtract、Multiplyをまとめて確認しているため、テスト名も曖昧です。

分けて書くと、意図が明確になります。

C#
[Fact]
public void Add_1And2_Returns3()
{
var calculator = new Calculator();

var result = calculator.Add(1, 2);

Assert.Equal(3, result);
}

10-3. Arrange-Act-Assertを明確に分ける

テストコードでは、Arrange、Act、Assertを明確に分けると読みやすくなります。

C#
[Fact]
public void IsFreeShipping_TotalPriceIs5000_ReturnsTrue()
{
// Arrange
var calculator = new PriceCalculator();

// Act
var result = calculator.IsFreeShipping(5000);

// Assert
Assert.True(result);
}

この構造にすると、テストの前提、実行、検証がひと目でわかります。

Actの部分は、できるだけ1行にすると理想的です。テスト対象の操作が明確になるため、何を検証しているテストなのかが伝わりやすくなります。

10-4. テストデータをわかりやすく管理する

テストデータは、読み手が理解しやすい形で管理します。意味のない値を使うと、なぜその値なのかがわからなくなります。

たとえば、送料無料の境界値をテストするなら、5000という値には意味があります。その場合は、定数名を付けると意図が伝わりやすくなります。

C#
private const int FreeShippingThreshold = 5000;

ただし、すべての値を定数化すればよいわけではありません。テスト内に直接値を書いたほうが読みやすい場合もあります。

テストデータが複雑な場合は、テスト用のファクトリメソッドを用意する方法もあります。

C#
private static User CreateActiveUser()
{
return new User
{
Name = "Test User",
IsActive = true
};
}

10-5. テストの独立性を保つ

テストは、それぞれ独立して実行できるようにします。あるテストが先に実行されることを前提にしてはいけません。

悪い例として、1つ目のテストでデータを作成し、2つ目のテストでそのデータを使うような構成があります。このようなテストは、実行順序が変わると失敗します。

各テストは、自分に必要なデータを自分で準備し、実行後に他のテストへ影響を残さないようにします。共有状態、静的変数、ファイル、データベースなどを使う場合は特に注意が必要です。

10-6. 壊れやすいテストを避ける

壊れやすいテストとは、本質的な仕様変更ではないのに頻繁に失敗するテストです。たとえば、現在時刻、実行順序、ランダム値、外部API、ファイルパス、OS依存の改行コードなどに依存するテストは不安定になりやすいです。

現在時刻を使う処理では、DateTime.Nowを直接呼ぶのではなく、時刻を提供するインターフェースを用意するとテストしやすくなります。

C#
public interface IClock
{
DateTime Now { get; }
}

テストでは固定時刻を返すフェイクを使えば、毎回同じ結果を検証できます。

壊れやすいテストが増えると、チームはテスト失敗を信用しなくなります。安定して実行でき、失敗したら本当に問題があると判断できるテストを目指しましょう。

11. C# Testingでよくあるエラーと解決方法

C# Testingを始めたばかりの頃は、テストが検出されない、参照できない、実行時にエラーになるといった問題がよく起こります。ここでは、代表的な原因と解決方法を整理します。

11-1. テストが検出されない原因

テストが検出されない場合、まず属性が正しく付いているか確認します。

xUnitなら[Fact]または[Theory]、NUnitなら[Test]または[TestCase]、MSTestなら[TestClass][TestMethod]が必要です。

次に、テスト実行に必要なパッケージが入っているか確認します。特に、Microsoft.NET.Test.Sdkや各フレームワークのテストアダプターが不足していると、テストが検出されないことがあります。

また、ビルドエラーがある場合もテストは正しく検出されません。まずは以下を実行して、ビルドが成功するか確認しましょう。

Bash
dotnet build

その後、テストを実行します。

Bash
dotnet test

11-2. プロジェクト参照ができない原因

テストプロジェクトから本体プロジェクトのクラスを参照できない場合、プロジェクト参照が追加されていない可能性があります。

以下のコマンドで参照を追加します。

Bash
dotnet add SampleApp.Tests/SampleApp.Tests.csproj reference SampleApp/SampleApp.csproj

また、テスト対象のクラスやメソッドがpublicになっているかも確認します。別プロジェクトから呼び出す場合、基本的にはpublicである必要があります。

名前空間のusingが不足している場合もあります。テストコードの先頭に、本体プロジェクトの名前空間を追加しましょう。

C#
using SampleApp;

11-3. NuGetパッケージ不足による実行エラー

テスト実行時にパッケージ関連のエラーが出る場合は、必要なNuGetパッケージが不足している可能性があります。

xUnitの場合は、以下を確認します。

Bash
dotnet add package xunit
dotnet add package xunit.runner.visualstudio
dotnet add package Microsoft.NET.Test.Sdk

NUnitの場合です。

Bash
dotnet add package NUnit
dotnet add package NUnit3TestAdapter
dotnet add package Microsoft.NET.Test.Sdk

MSTestの場合です。

Bash
dotnet add package MSTest.TestFramework
dotnet add package MSTest.TestAdapter
dotnet add package Microsoft.NET.Test.Sdk

テンプレートから作成していれば通常は自動で追加されますが、手動でプロジェクトを作った場合や、古いプロジェクトを移行した場合は不足していることがあります。

11-4. asyncメソッドのテストで失敗する原因

非同期メソッドをテストする場合は、テストメソッドもasync Taskにします。async voidは避けましょう。

良い例です。

C#
[Fact]
public async Task GetUserAsync_ExistingId_ReturnsUser()
{
var service = new UserService();

var user = await service.GetUserAsync(1);

Assert.NotNull(user);
}

例外を検証する場合は、xUnitならAssert.ThrowsAsyncを使います。

C#
await Assert.ThrowsAsync<ArgumentException>(async () =>
{
await service.GetUserAsync(-1);
});

非同期処理のテストで失敗する原因として、awaitを付け忘れている、Taskの完了前にAssertしている、同期的に.Result.Wait()を使ってデッドロックを起こしている、といったものがあります。

11-5. テスト実行順序に依存して失敗する原因

テストは実行順序に依存しないように書く必要があります。テストフレームワークは、必ずしもファイル順やメソッド順にテストを実行するとは限りません。

実行順序に依存するテストは、単体で実行すると成功するのに、全体で実行すると失敗することがあります。

原因としては、以下が考えられます。

・静的変数の状態を共有している
・同じファイルを書き換えている
・同じデータベースレコードを使っている
・前のテストで作成したデータを次のテストが使っている
・後片付けができていない

各テストは必要なデータを自分で準備し、他のテストに影響を与えないようにしましょう。

11-6. ローカルでは成功するのにCIで失敗する原因

ローカルでは成功するのにCIで失敗する場合、環境差異が原因であることが多いです。

よくある原因は、OSの違い、ファイルパスの違い、タイムゾーンの違い、環境変数の不足、外部サービスへの接続失敗、テスト実行順序の違いです。

たとえば、Windowsでは成功するがLinuxのCIで失敗する場合、パス区切り文字や大文字小文字の扱いが原因かもしれません。日付関連のテストでは、タイムゾーンや現在時刻への依存が問題になることがあります。

CIで安定してテストを実行するには、環境に依存しないテストを書くことが重要です。外部APIやデータベースに依存するテストは、単体テストではなく結合テストとして分け、必要な環境を明確に準備しましょう。

12. C# Testingを現場で活用するための次のステップ

単体テストの基本を理解したら、次は現場で活用するための考え方を学びましょう。テストは書いて終わりではなく、開発プロセスの中で継続的に使うことで価値を発揮します。

12-1. テスト駆動開発の基本

テスト駆動開発、いわゆるTDDは、先にテストを書き、そのテストを通すために実装する開発手法です。

基本の流れは、以下の3ステップです。

1. 失敗するテストを書く
2. テストが通る最小限の実装をする
3. リファクタリングする

最初に失敗するテストを書くことで、これから実装する仕様を明確にします。その後、テストが通るように実装し、最後にコードをきれいに整えます。

TDDは最初から完璧に実践する必要はありません。まずは、バグ修正時に再現テストを書いてから修正する、追加機能の重要な条件だけ先にテストする、といった形で取り入れると始めやすいです。

12-2. カバレッジ計測の考え方

テストカバレッジは、コードのうちどの程度がテストで実行されたかを示す指標です。カバレッジを計測すると、テストが不足している箇所を見つけやすくなります。

ただし、カバレッジが高いからといって、必ず品質が高いとは限りません。単にコードを通過するだけで、正しい結果を検証していないテストもあり得ます。

大切なのは、カバレッジの数値だけを目標にするのではなく、重要な業務ロジック、条件分岐、例外処理、境界値が適切に検証されているかを見ることです。

カバレッジは、品質を保証する数字ではなく、テストの抜け漏れを見つけるための補助指標として使いましょう。

12-3. リファクタリングと単体テストの関係

リファクタリングとは、外部から見た動作を変えずにコードの内部構造を改善することです。単体テストは、リファクタリングを安全に行うための重要な土台になります。

テストがない状態でコードを整理すると、動作が変わっていないかを手動で確認しなければなりません。一方、単体テストがあれば、変更後にテストを実行することで、既存の振る舞いが保たれているかを確認できます。

特に、長く運用されているC#プロジェクトでは、複雑なクラスや巨大なメソッドが増えがちです。いきなり大きく書き換えるのではなく、まず重要な振る舞いに対するテストを書き、その後で少しずつリファクタリングすると安全です。

12-4. ASP.NET Coreアプリケーションのテストへ進む

C# Testingの基本を学んだら、ASP.NET Coreアプリケーションのテストにも進めます。ASP.NET Coreでは、コントローラー、サービス、ミドルウェア、Web APIのテストなどが対象になります。

サービスクラスの単体テストでは、ビジネスロジックを外部依存から切り離して検証します。コントローラーのテストでは、戻り値のActionResultやステータスコードを確認します。

さらに進むと、WebApplicationFactoryを使った統合テストもあります。これは、アプリケーションをテスト用に起動し、HTTPリクエストを送ってレスポンスを検証する方法です。

最初はサービス層の単体テストから始め、慣れてきたらWeb API全体のテストへ広げると学びやすいです。

12-5. Entity Framework Coreを使う処理のテストへ進む

Entity Framework Coreを使う処理では、データベースアクセスをどうテストするかが課題になります。

単体テストでは、リポジトリやDbContextへの依存を切り離し、モックやフェイクを使ってビジネスロジックを検証します。一方、実際のSQL発行やデータベースとの連携を確認したい場合は、結合テストとして扱います。

EF Coreのテストでは、インメモリデータベースやSQLiteのインメモリモードを使う方法があります。ただし、本番のデータベースと挙動が完全に同じとは限らないため、何を検証したいのかを明確にする必要があります。

ビジネスルールを検証したいなら単体テスト、実際のデータベース連携を確認したいなら結合テスト、というように目的を分けて考えましょう。

12-6. 保守しやすいC#プロジェクトに育てるための学習ロードマップ

C# Testingを現場で活用するためには、段階的に学ぶのがおすすめです。

まずは、xUnit、NUnit、MSTestのいずれか1つを選び、単純なメソッドの単体テストを書けるようにします。次に、例外、境界値、パラメータ化テストを学びます。

その後、モックを使った外部依存の分離、非同期メソッドのテスト、テストデータの管理、CIでの自動実行へ進みます。さらに、ASP.NET Core、Entity Framework Core、Web APIの統合テストを学ぶと、実務で必要なテストに対応しやすくなります。

学習の流れは以下のように考えるとよいでしょう。

1. 単純なメソッドの単体テスト
2. Assertと例外テスト
3. パラメータ化テスト
4. Arrange-Act-Assert
5. モックと依存性注入
6. asyncメソッドのテスト
7. CIでの自動テスト
8. ASP.NET Coreのテスト
9. EF Coreを含む結合テスト
10. カバレッジとリファクタリング

一度にすべてを覚える必要はありません。日々の開発で「変更が怖い」と感じる箇所からテストを書き始めることが、もっとも実践的な第一歩です。

まとめ

C# Testingは、C#アプリケーションの品質を高め、安心して変更できるコードベースを作るために欠かせない取り組みです。特に単体テストは、メソッドやクラスの振る舞いを小さな単位で確認できるため、初心者が最初に学ぶべきテストです。

C#の主要なテストフレームワークには、xUnit、NUnit、MSTestがあります。xUnitはシンプルでモダン、NUnitは高機能で柔軟、MSTestはMicrosoft標準でVisual Studioとの相性が良いという特徴があります。新規プロジェクトではxUnitが選ばれることが多いですが、既存プロジェクトではすでに使われているフレームワークに合わせるのが基本です。

テストコードを書くときは、Arrange-Act-Assertを意識し、正常系、異常系、境界値、例外を検証しましょう。外部依存がある場合は、インターフェースやモックを使ってテスト対象を分離します。また、テスト名、テストデータ、独立性、壊れにくさを意識することで、保守しやすいテストコードになります。

最初から完璧なテストを目指す必要はありません。まずは、重要な業務ロジックや変更頻度の高い処理に対して、1つずつ単体テストを書いてみましょう。C# Testingを継続的に取り入れることで、リファクタリングしやすく、バグに強く、長く保守できるC#プロジェクトへ育てていけます。