C#ユニットテスト入門:初心者がつまずく書き方・使い方・おすすめフレームワークを徹底解説
はじめに
C#でアプリケーションを開発していると、「この処理、本当に正しく動いているだろうか」「修正したら別の機能が壊れていないだろうか」と不安になる場面があります。そこで役立つのがユニットテストです。
ユニットテストを導入すると、メソッドやクラス単位で処理の正しさを自動確認できるようになります。手作業で画面を操作したり、毎回デバッグ実行したりしなくても、期待した結果になっているかを素早く検証できます。
一方で、C#のユニットテストを初めて学ぶ人は、「MSTest・NUnit・xUnitのどれを使えばいいのか」「Assertの書き方がわからない」「モックとは何か」「privateメソッドはテストするべきか」などでつまずきやすいです。
この記事では、C#ユニットテストの基礎から、フレームワークの選び方、Visual Studioや.NET CLIでの環境構築、基本的な書き方、実践例、初心者が失敗しやすいポイント、現場での活用方法までを順番に解説します。
1. C#のユニットテストとは?初心者向けに基礎を解説
1-1. ユニットテストの意味と目的
ユニットテストとは、プログラムを構成する小さな単位が期待通りに動くかを確認するテストです。C#では、主にメソッドやクラスを対象にしてテストを書くことが多いです。
たとえば、税込価格を計算するメソッドがある場合、「1000円を渡したら1100円が返る」「0円を渡したら0円が返る」「マイナス値を渡したら例外が発生する」といった確認をコードで自動化します。
ユニットテストの目的は、単にバグを見つけることだけではありません。主な目的は次の通りです。
処理の正しさを自動で確認すること。コード変更による影響を早期に発見すること。リファクタリングを安全に行うこと。仕様をテストコードとして残すこと。開発者が安心してコードを変更できる状態を作ること。
特にC#のように業務システム、Webアプリ、API、デスクトップアプリ、ゲーム開発など幅広い分野で使われる言語では、ユニットテストによる品質確保が重要になります。
1-2. 単体テスト・結合テスト・E2Eテストの違い
ユニットテストを理解するには、ほかのテストとの違いを知っておくことが大切です。
単体テストは、メソッドやクラスなど小さな単位を確認するテストです。外部API、データベース、ファイルシステムなどへの依存はできるだけ切り離し、対象のロジックだけを検証します。
結合テストは、複数のクラスやモジュールを組み合わせたときに正しく動くかを確認するテストです。たとえば、サービスクラスとリポジトリクラスを組み合わせ、データベースとの連携まで確認するケースがあります。
E2Eテストは、ユーザーの操作に近い形でシステム全体を確認するテストです。Webアプリであれば、ブラウザを開いてログインし、商品を検索し、注文を完了するような流れを検証します。
ユニットテストは最も小さく、最も高速に実行しやすいテストです。そのため、開発中に何度も実行しやすく、問題を早い段階で発見できます。
1-3. C#開発でユニットテストが重要な理由
C#開発でユニットテストが重要な理由は、変更に強いコードを作れるからです。
開発現場では、一度書いたコードを後から修正することがよくあります。機能追加、仕様変更、バグ修正、パフォーマンス改善、リファクタリングなど、コードは継続的に変化します。そのたびに手動確認だけで品質を保とうとすると、確認漏れが発生しやすくなります。
ユニットテストがあれば、変更後にテストを実行するだけで、既存の重要な動作が壊れていないかを確認できます。特にビジネスロジックが複雑なC#アプリケーションでは、ユニットテストが安全網として機能します。
また、C#ではVisual Studioのテストエクスプローラーやdotnet testコマンドを使って、テストを簡単に実行できます。CI/CDにも組み込みやすいため、チーム開発でも自動テストを活用しやすい環境が整っています。
1-4. ユニットテストで確認すべき範囲と確認しない範囲
ユニットテストでは、主にロジックの正しさを確認します。たとえば、計算処理、条件分岐、文字列変換、入力値チェック、状態変更、例外発生条件などです。
一方で、ユニットテストで確認しない方がよい範囲もあります。データベースに本当に保存されたか、外部APIが実際に応答するか、画面表示が崩れていないか、ネットワークが正常につながるかといった内容は、ユニットテストではなく結合テストやE2Eテストで確認する方が適しています。
ユニットテストは「小さく、速く、安定して実行できること」が重要です。外部環境に依存しすぎると、テストが遅くなったり、環境によって失敗したりします。そのため、外部依存はモックやスタブに置き換え、テスト対象のロジックに集中するのが基本です。
2. C#ユニットテストを始める前に知っておきたい基本用語
2-1. テスト対象・テストケース・アサーションとは
C#ユニットテストでは、いくつかの基本用語を理解しておくと学習がスムーズになります。
テスト対象とは、テストしたいメソッドやクラスのことです。たとえば、PriceCalculatorクラスのCalculateTaxIncludedPriceメソッドをテストする場合、このメソッドがテスト対象になります。
テストケースとは、どのような条件で何を確認するかを表す単位です。たとえば、「価格1000円、税率10%のとき、税込価格1100円を返す」という確認が1つのテストケースです。
アサーションとは、実際の結果が期待値と一致しているかを確認する処理です。C#のユニットテストでは、Assert.Equal、Assert.AreEqual、Assert.Thatなどを使って結果を検証します。
たとえばxUnitでは、次のように書きます。
C#Assert.Equal(1100, result);
これは、「resultが1100であることを期待する」という意味です。期待値と実際の値が異なる場合、テストは失敗します。
2-2. Arrange・Act・Assert(AAA)パターンとは
C#ユニットテストでは、Arrange・Act・Assert、通称AAAパターンを意識すると読みやすいテストになります。
Arrangeは、テストの準備です。テスト対象のインスタンスを作成したり、入力値を用意したりします。
Actは、実際にテスト対象の処理を実行する部分です。メソッドを呼び出し、戻り値を受け取ります。
Assertは、結果を検証する部分です。期待した戻り値になっているか、例外が発生したか、依存オブジェクトのメソッドが呼ばれたかなどを確認します。
例を見てみましょう。
C#[Fact]
public void Add_2と3を渡すと5を返す()
{
// Arrange
var calculator = new Calculator();
// Act
var result = calculator.Add(2, 3);
// Assert
Assert.Equal(5, result);
}
このように3つの役割を分けると、テストの意図が明確になります。初心者は、まずAAAパターンに沿って書くことを意識するとよいでしょう。
2-3. テストメソッド・テストクラス・テストプロジェクトの役割
C#のユニットテストは、通常、テストプロジェクトの中にテストクラスを作り、その中にテストメソッドを書きます。
テストプロジェクトは、テストコードを管理するためのプロジェクトです。アプリケーション本体のプロジェクトとは分けて作成するのが一般的です。たとえば、アプリ本体がSampleAppなら、テストプロジェクトはSampleApp.Testsのような名前にします。
テストクラスは、関連するテストメソッドをまとめるクラスです。たとえば、Calculatorクラスをテストする場合は、CalculatorTestsというテストクラスを作ります。
テストメソッドは、具体的な1つの動作を確認するメソッドです。xUnitなら[Fact]、NUnitなら[Test]、MSTestなら[TestMethod]といった属性を付けて、テストランナーが実行できるようにします。
構成例は次の通りです。
C#public class CalculatorTests
{
[Fact]
public void Add_2と3を渡すと5を返す()
{
var calculator = new Calculator();
var result = calculator.Add(2, 3);
Assert.Equal(5, result);
}
}
このように、プロジェクト、クラス、メソッドの役割を分けることで、テストコードを整理しやすくなります。
2-4. テストダブル・モック・スタブ・フェイクの違い
ユニットテストでは、外部依存を本物の代わりに置き換えることがあります。この置き換え用のオブジェクトを総称してテストダブルと呼びます。
スタブは、決まった値を返すための代用品です。たとえば、現在日時を返すインターフェースに対して、常に2026年1月1日を返すスタブを用意するようなケースです。
モックは、呼び出し内容を検証するための代用品です。たとえば、「メール送信メソッドが1回呼ばれたこと」を確認したい場合に使います。
フェイクは、簡易的に動作する実装です。たとえば、本物のデータベースの代わりに、メモリ上のリストにデータを保存する簡易リポジトリを使うようなケースです。
初心者は、まず「外部依存を置き換えるものがテストダブル」「戻り値を用意するのがスタブ」「呼び出しを確認するのがモック」と理解するとよいでしょう。
3. C#ユニットテストのおすすめフレームワーク比較
3-1. MSTestの特徴・メリット・向いているケース
MSTestは、Microsoftが提供しているC#向けのテストフレームワークです。Visual Studioとの親和性が高く、標準的な選択肢として使いやすいのが特徴です。
MSTestでは、テストクラスに[TestClass]、テストメソッドに[TestMethod]を付けます。
C#[TestClass]
public class CalculatorTests
{
[TestMethod]
public void Add_2と3を渡すと5を返す()
{
var calculator = new Calculator();
var result = calculator.Add(2, 3);
Assert.AreEqual(5, result);
}
}
MSTestのメリットは、Visual Studioを中心に開発している人にとって導入しやすいことです。Microsoft公式の安心感があり、企業の開発現場でも採用しやすいフレームワークです。
一方で、後述するxUnitやNUnitに比べると、書き味や柔軟性の面で好みが分かれることがあります。とはいえ、C#ユニットテストの基本を学ぶには十分な機能があります。
MSTestは、Visual Studio中心の開発、Microsoft標準を重視するチーム、社内ルールでMicrosoft系ツールを優先する現場に向いています。
3-2. NUnitの特徴・メリット・向いているケース
NUnitは、.NETで長く使われてきたテストフレームワークです。豊富なアサーション機能や柔軟なテスト記述が特徴です。
NUnitでは、テストメソッドに[Test]を付けます。
C#[TestFixture]
public class CalculatorTests
{
[Test]
public void Add_2と3を渡すと5を返す()
{
var calculator = new Calculator();
var result = calculator.Add(2, 3);
Assert.That(result, Is.EqualTo(5));
}
}
NUnitのメリットは、表現力の高いアサーションが使えることです。Assert.Thatを使うことで、「何を期待しているのか」を自然に書きやすくなります。
また、複数パターンのテストを[TestCase]で簡潔に書ける点も便利です。
C#[TestCase(2, 3, 5)]
[TestCase(10, 20, 30)]
public void Add_2つの数値を渡すと合計を返す(int a, int b, int expected)
{
var calculator = new Calculator();
var result = calculator.Add(a, b);
Assert.That(result, Is.EqualTo(expected));
}
NUnitは、長く使われている実績を重視したい場合、柔軟にテストを書きたい場合、既存プロジェクトでNUnitが使われている場合に向いています。
3-3. xUnitの特徴・メリット・向いているケース
xUnitは、近年の.NET開発でよく使われるテストフレームワークです。シンプルでモダンな設計が特徴で、ASP.NET Coreなどのプロジェクトでも採用されることが多いです。
xUnitでは、通常のテストに[Fact]、パラメータ付きテストに[Theory]を使います。
C#public class CalculatorTests
{
[Fact]
public void Add_2と3を渡すと5を返す()
{
var calculator = new Calculator();
var result = calculator.Add(2, 3);
Assert.Equal(5, result);
}
}
複数パターンを確認する場合は、次のように書きます。
C#[Theory]
[InlineData(2, 3, 5)]
[InlineData(10, 20, 30)]
public void Add_2つの数値を渡すと合計を返す(int a, int b, int expected)
{
var calculator = new Calculator();
var result = calculator.Add(a, b);
Assert.Equal(expected, result);
}
xUnitのメリットは、余計な属性が少なく、シンプルに書けることです。また、コンストラクタやIDisposableを使ったセットアップ・後処理の考え方など、現代的な設計になっています。
xUnitは、これから新しくC#ユニットテストを始める人、ASP.NET Coreなどのモダンな.NET開発をしている人、シンプルな書き方を好む人に向いています。
3-4. 初心者はどれを選ぶべき?フレームワーク選定の基準
初心者がC#ユニットテストを始めるなら、基本的にはxUnitまたはMSTestがおすすめです。
Visual Studio中心で、Microsoft標準に近い形で学びたいならMSTestが使いやすいです。属性名もわかりやすく、初めてでも構造を理解しやすいでしょう。
一方で、新しい.NETプロジェクトやWeb API開発を学んでいるなら、xUnitがおすすめです。書き方がシンプルで、情報も多く、実務でもよく使われます。
NUnitは、既存プロジェクトで使われている場合や、Assert.ThatやTestCaseの書き方が好みに合う場合に選ぶとよいでしょう。
選定基準は、次のように考えるとわかりやすいです。
Visual Studioとの親和性を重視するならMSTest。柔軟な書き方や長年の実績を重視するならNUnit。シンプルでモダンな書き方を重視するならxUnit。チームで使っているものがあるなら、そのフレームワークに合わせる。
初心者が迷った場合は、まずxUnitで基本を学ぶとよいです。ただし、どのフレームワークを選んでも、ユニットテストの本質は大きく変わりません。大切なのは、フレームワーク選びよりも「何をどう検証するか」です。
3-5. MSTest・NUnit・xUnitの書き方の違い
同じテストでも、フレームワークによって属性やAssertの書き方が少し異なります。
MSTestの場合は次のように書きます。
C#[TestClass]
public class CalculatorTests
{
[TestMethod]
public void Add_2と3を渡すと5を返す()
{
var calculator = new Calculator();
var result = calculator.Add(2, 3);
Assert.AreEqual(5, result);
}
}
NUnitの場合は次のようになります。
C#[TestFixture]
public class CalculatorTests
{
[Test]
public void Add_2と3を渡すと5を返す()
{
var calculator = new Calculator();
var result = calculator.Add(2, 3);
Assert.That(result, Is.EqualTo(5));
}
}
xUnitの場合は次のように書きます。
C#public class CalculatorTests
{
[Fact]
public void Add_2と3を渡すと5を返す()
{
var calculator = new Calculator();
var result = calculator.Add(2, 3);
Assert.Equal(5, result);
}
}
違いはありますが、どれも「準備して、実行して、結果を確認する」という流れは同じです。初心者は、まず1つのフレームワークに絞って慣れるのがおすすめです。
4. C#ユニットテストの環境構築と実行方法
4-1. Visual Studioでテストプロジェクトを作成する方法
Visual Studioを使う場合、C#ユニットテストの環境構築は比較的簡単です。
まず、ソリューションを右クリックし、「追加」から「新しいプロジェクト」を選択します。次に、検索欄で「テスト」と入力し、使用したいテストプロジェクトを選びます。たとえば、xUnitを使うなら「xUnit テスト プロジェクト」、MSTestを使うなら「MSTest テスト プロジェクト」を選択します。
プロジェクト名は、テスト対象のプロジェクト名に.Testsを付けるとわかりやすいです。たとえば、SampleAppというプロジェクトをテストするなら、SampleApp.Testsという名前にします。
テストプロジェクトを作成したら、テスト対象のプロジェクトを参照に追加します。これにより、テストコードから本体プロジェクトのクラスやメソッドを呼び出せるようになります。
Visual Studioでは、テストエクスプローラーを使ってテストを一覧表示し、クリック操作で実行できます。初心者でも始めやすい方法です。
4-2. .NET CLIでテストプロジェクトを作成する方法
Visual Studioを使わない場合や、コマンドラインで作業したい場合は、.NET CLIを使ってテストプロジェクトを作成できます。
xUnitのテストプロジェクトを作成する例は次の通りです。
Bashdotnet new xunit -n SampleApp.Tests
MSTestの場合は次のようにします。
Bashdotnet new mstest -n SampleApp.Tests
NUnitの場合は次のようにします。
Bashdotnet new nunit -n SampleApp.Tests
ソリューションにテストプロジェクトを追加する場合は、次のコマンドを使います。
Bashdotnet sln add SampleApp.Tests/SampleApp.Tests.csproj
.NET CLIを使うと、Visual Studio CodeやJetBrains Riderなど、Visual Studio以外の環境でもC#ユニットテストを始められます。また、CI/CD環境でも同じコマンドを使えるため、チーム開発でも便利です。
4-3. テスト対象プロジェクトを参照する方法
テストプロジェクトからアプリ本体のコードを呼び出すには、テスト対象プロジェクトへの参照を追加する必要があります。
.NET CLIでは、次のように実行します。
Bashdotnet add SampleApp.Tests/SampleApp.Tests.csproj reference SampleApp/SampleApp.csproj
これにより、SampleApp.TestsからSampleApp内のpublicなクラスやメソッドを使用できるようになります。
Visual Studioの場合は、テストプロジェクトの「依存関係」または「参照」を右クリックし、「プロジェクト参照の追加」からテスト対象プロジェクトを選択します。
参照を追加したら、テストコード側で必要な名前空間をusingします。
C#using SampleApp;
注意点として、テストコードから直接呼び出せるのは基本的にpublicなクラスやメソッドです。privateメソッドを直接テストしようとするのではなく、publicメソッドを通じて動作を確認するのが一般的です。
4-4. テストエクスプローラーでテストを実行する方法
Visual Studioでは、テストエクスプローラーからC#ユニットテストを実行できます。
メニューから「テスト」→「テスト エクスプローラー」を開くと、プロジェクト内のテストが一覧表示されます。テストメソッドの属性が正しく付いていれば、自動的に検出されます。
実行方法は簡単です。すべてのテストを実行する場合は「すべて実行」をクリックします。特定のテストだけを実行したい場合は、そのテストを選んで実行します。
テストが成功すると緑色、失敗すると赤色で表示されます。失敗した場合は、期待値と実際の値、例外メッセージ、失敗した行番号などを確認できます。
テストエクスプローラーを使うと、デバッグ実行も可能です。失敗原因がわからない場合は、テストメソッドにブレークポイントを置いてデバッグすると原因を追いやすくなります。
4-5. dotnet testコマンドでテストを実行する方法
.NET CLIでは、dotnet testコマンドでテストを実行できます。
ソリューション全体のテストを実行する場合は、ソリューションファイルがあるディレクトリで次のコマンドを実行します。
Bashdotnet test
特定のテストプロジェクトだけを実行する場合は、次のように指定します。
Bashdotnet test SampleApp.Tests/SampleApp.Tests.csproj
dotnet testは、CI/CDでよく使われます。GitHub Actions、Azure DevOps、GitLab CIなどの自動化環境でこのコマンドを実行すれば、プッシュやプルリクエストのたびにテストを自動実行できます。
テストが1つでも失敗すると、コマンドの終了コードが失敗扱いになります。そのため、ビルドやデプロイ前の品質チェックとして活用できます。
5. C#ユニットテストの基本的な書き方
5-1. 最小構成のテストコード例
まずは、C#ユニットテストの最小構成を見てみましょう。ここではxUnitを例にします。
テスト対象のクラスは次の通りです。
C#public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
}
このAddメソッドをテストするコードは次のようになります。
C#public class CalculatorTests
{
[Fact]
public void Add_2と3を渡すと5を返す()
{
var calculator = new Calculator();
var result = calculator.Add(2, 3);
Assert.Equal(5, result);
}
}
このテストでは、Calculatorを作成し、Add(2, 3)を実行し、結果が5であることを確認しています。
初心者は、まずこの形を覚えるとよいでしょう。複雑なテストを書こうとする前に、シンプルなメソッドを対象にして「入力」「実行」「期待結果」を明確にすることが大切です。
5-2. Assertの基本的な使い方
Assertは、ユニットテストで結果を確認するための重要な機能です。xUnitを例にすると、代表的なAssertには次のようなものがあります。
C#Assert.Equal(expected, actual);
Assert.NotEqual(expected, actual);
Assert.True(condition);
Assert.False(condition);
Assert.Null(value);
Assert.NotNull(value);
Assert.Contains(expectedSubstring, actualString);
Assert.Empty(collection);
Assert.NotEmpty(collection);
たとえば、戻り値が期待通りか確認する場合は次のように書きます。
C#Assert.Equal("山田太郎", user.Name);
条件が真であることを確認する場合は次のように書きます。
C#Assert.True(user.IsActive);
コレクションに要素が含まれているか確認する場合は、次のように書けます。
C#Assert.Contains(users, user => user.Name == "山田太郎");
Assertを書くときは、「何を期待しているのか」が読み手に伝わるようにすることが大切です。複雑な条件を1行に詰め込みすぎると、失敗したときに原因がわかりにくくなります。
5-3. 例外が発生する処理をテストする方法
C#ユニットテストでは、例外が正しく発生するかも確認できます。
たとえば、割り算を行うメソッドで、0で割ろうとした場合にArgumentExceptionを投げるとします。
C#public class Calculator
{
public int Divide(int a, int b)
{
if (b == 0)
{
throw new ArgumentException("0で割ることはできません。");
}
return a / b;
}
}
xUnitでは、例外のテストを次のように書きます。
C#[Fact]
public void Divide_0で割ろうとするとArgumentExceptionを投げる()
{
var calculator = new Calculator();
var exception = Assert.Throws<ArgumentException>(() => calculator.Divide(10, 0));
Assert.Equal("0で割ることはできません。", exception.Message);
}
このテストでは、Divide(10, 0)を実行したときにArgumentExceptionが発生することを確認しています。さらに、例外メッセージも検証しています。
例外のテストは、入力値チェックや業務ルールの検証でよく使います。異常系のテストをしっかり書くことで、想定外の入力に対して安全に動くコードを作りやすくなります。
5-4. 複数パターンを効率よくテストする方法
同じメソッドに対して複数の入力パターンを確認したい場合、似たようなテストメソッドを大量に書くと保守が大変になります。
xUnitでは、[Theory]と[InlineData]を使うことで、複数パターンを効率よくテストできます。
C#[Theory]
[InlineData(2, 3, 5)]
[InlineData(0, 0, 0)]
[InlineData(-1, 1, 0)]
[InlineData(10, -3, 7)]
public void Add_2つの数値を渡すと合計を返す(int a, int b, int expected)
{
var calculator = new Calculator();
var result = calculator.Add(a, b);
Assert.Equal(expected, result);
}
このように書くと、1つのテストメソッドで複数の入力値を検証できます。
NUnitでは[TestCase]、MSTestでは[DataTestMethod]と[DataRow]を使って同様のことができます。
MSTestの例は次の通りです。
C#[DataTestMethod]
[DataRow(2, 3, 5)]
[DataRow(0, 0, 0)]
[DataRow(-1, 1, 0)]
public void Add_2つの数値を渡すと合計を返す(int a, int b, int expected)
{
var calculator = new Calculator();
var result = calculator.Add(a, b);
Assert.AreEqual(expected, result);
}
同じ観点で入力値だけが違う場合は、パラメータ化テストを活用しましょう。
5-5. privateメソッドはテストすべきか
初心者がよく悩むのが、privateメソッドをテストすべきかどうかです。
基本的には、privateメソッドを直接テストする必要はありません。privateメソッドはクラス内部の実装詳細であり、外部から見える振る舞いではないからです。
ユニットテストでは、publicメソッドを通じてクラスの振る舞いを確認します。privateメソッドが正しく動いているかは、それを利用しているpublicメソッドのテストで間接的に確認できます。
もしprivateメソッドを直接テストしたくなる場合、そのメソッドに重要なロジックが詰まりすぎている可能性があります。その場合は、別のクラスに切り出してpublicメソッドとしてテストできる形にすることを検討しましょう。
ただし、無理に設計を変える必要はありません。判断基準は「外部から見た振る舞いとしてテストできるか」です。privateメソッドそのものではなく、利用者にとって意味のある動作をテストすることが大切です。
6. 実践例で学ぶC#ユニットテスト
6-1. 戻り値のあるメソッドをテストする
戻り値のあるメソッドは、C#ユニットテストの中でも最もテストしやすい対象です。
たとえば、税込価格を計算するクラスを考えます。
C#public class PriceCalculator
{
public int CalculateTaxIncludedPrice(int price, decimal taxRate)
{
return (int)(price * (1 + taxRate));
}
}
このメソッドをテストするコードは次の通りです。
C#public class PriceCalculatorTests
{
[Fact]
public void CalculateTaxIncludedPrice_価格1000税率10パーセントなら1100を返す()
{
var calculator = new PriceCalculator();
var result = calculator.CalculateTaxIncludedPrice(1000, 0.10m);
Assert.Equal(1100, result);
}
}
戻り値のあるメソッドでは、入力値と期待結果を明確にすることが重要です。正常系だけでなく、0、境界値、異常値なども確認すると、より信頼性の高いテストになります。
6-2. 条件分岐のあるメソッドをテストする
条件分岐があるメソッドでは、分岐ごとにテストケースを用意します。
たとえば、会員ランクによって割引率を返すメソッドを考えます。
C#public class DiscountService
{
public decimal GetDiscountRate(string memberRank)
{
return memberRank switch
{
"Gold" => 0.20m,
"Silver" => 0.10m,
"Bronze" => 0.05m,
_ => 0.00m
};
}
}
この場合、各ランクごとに期待される割引率を確認します。
C#public class DiscountServiceTests
{
[Theory]
[InlineData("Gold", 0.20)]
[InlineData("Silver", 0.10)]
[InlineData("Bronze", 0.05)]
[InlineData("Normal", 0.00)]
public void GetDiscountRate_会員ランクに応じた割引率を返す(string rank, double expected)
{
var service = new DiscountService();
var result = service.GetDiscountRate(rank);
Assert.Equal((decimal)expected, result);
}
}
条件分岐のテストでは、すべての分岐を最低1回は通るようにすると安心です。特にelseやdefaultのような例外的なルートは見落とされやすいため、意識してテストしましょう。
6-3. 日付・時刻を扱う処理をテストする
日付や時刻を扱う処理は、ユニットテストでつまずきやすいポイントです。理由は、DateTime.Nowを直接使うと、テストを実行するタイミングによって結果が変わるからです。
たとえば、次のようなコードはテストしにくいです。
C#public bool IsExpired(DateTime expirationDate)
{
return expirationDate < DateTime.Now;
}
このままだと、現在時刻をテスト側で制御できません。そこで、現在時刻を取得する処理をインターフェースに切り出します。
C#public interface IClock
{
DateTime Now { get; }
}
public class SystemClock : IClock
{
public DateTime Now => DateTime.Now;
}
public class ExpirationService
{
private readonly IClock _clock;
public ExpirationService(IClock clock)
{
_clock = clock;
}
public bool IsExpired(DateTime expirationDate)
{
return expirationDate < _clock.Now;
}
}
テストでは、固定の時刻を返すフェイクを使います。
C#public class FakeClock : IClock
{
public DateTime Now { get; set; }
}
public class ExpirationServiceTests
{
[Fact]
public void IsExpired_期限日が現在時刻より前ならtrueを返す()
{
var clock = new FakeClock
{
Now = new DateTime(2026, 1, 10)
};
var service = new ExpirationService(clock);
var result = service.IsExpired(new DateTime(2026, 1, 1));
Assert.True(result);
}
}
日付・時刻を扱うC#ユニットテストでは、「現在時刻を固定できる設計」にすることが重要です。
6-4. 外部API・DB・ファイル操作を含む処理をテストする
外部API、データベース、ファイル操作を含む処理は、そのままユニットテストに含めると不安定になりやすいです。
たとえば、外部APIの応答が遅い、ネットワークが不安定、データベースの状態が変わる、ファイルが存在しないなどの理由で、テストが失敗する可能性があります。
ユニットテストでは、こうした外部依存をインターフェースで抽象化し、テスト時にはモックやフェイクに置き換えるのが基本です。
たとえば、ユーザー情報を取得する処理を考えます。
C#public interface IUserRepository
{
User FindById(int id);
}
public class UserService
{
private readonly IUserRepository _repository;
public UserService(IUserRepository repository)
{
_repository = repository;
}
public string GetUserName(int id)
{
var user = _repository.FindById(id);
if (user == null)
{
return "Unknown";
}
return user.Name;
}
}
このようにリポジトリをインターフェースにしておくと、テスト時に本物のDBではなく、テスト用の実装やモックを使えます。
ユニットテストでは外部環境に依存しないことが大切です。本物のAPIやDBを使った確認は、結合テストとして分けて実施しましょう。
6-5. Moqを使って依存関係をモック化する
C#でモックを使う場合、よく利用されるライブラリの1つがMoqです。Moqを使うと、インターフェースの代用品を簡単に作成できます。
先ほどのIUserRepositoryをMoqでモック化してみます。
C#public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
テストコードは次のようになります。
C#using Moq;
using Xunit;
public class UserServiceTests
{
[Fact]
public void GetUserName_ユーザーが存在する場合は名前を返す()
{
var repositoryMock = new Mock<IUserRepository>();
repositoryMock
.Setup(x => x.FindById(1))
.Returns(new User { Id = 1, Name = "山田太郎" });
var service = new UserService(repositoryMock.Object);
var result = service.GetUserName(1);
Assert.Equal("山田太郎", result);
}
[Fact]
public void GetUserName_ユーザーが存在しない場合はUnknownを返す()
{
var repositoryMock = new Mock<IUserRepository>();
repositoryMock
.Setup(x => x.FindById(999))
.Returns((User?)null);
var service = new UserService(repositoryMock.Object);
var result = service.GetUserName(999);
Assert.Equal("Unknown", result);
}
}
Moqでは、Setupでメソッドが呼ばれたときの戻り値を指定し、Objectでモックの実体を取得します。
また、メソッドが呼ばれたことを確認したい場合はVerifyを使います。
C#repositoryMock.Verify(x => x.FindById(1), Times.Once);
ただし、モックを使いすぎるとテストが実装詳細に依存しやすくなります。戻り値や外部から見た振る舞いを確認できる場合は、過剰にVerifyしすぎないようにしましょう。
7. 初心者がC#ユニットテストでつまずきやすいポイント
7-1. 何をテストすればよいかわからない
初心者が最初につまずきやすいのは、「何をテストすればよいかわからない」という点です。
すべての行をテストしようとすると大変ですし、逆に簡単な処理だからと何もテストしないと、重要なバグを見逃すことがあります。
まずは、ビジネス上重要なロジックからテストしましょう。たとえば、金額計算、割引判定、権限チェック、入力値検証、ステータス変更、日付判定などです。
テストケースを考えるときは、正常系、異常系、境界値の3つを意識すると整理しやすくなります。
正常系は、通常の入力で期待通りに動くかを確認するテストです。異常系は、不正な入力や想定外の状態に対して適切に処理できるかを確認します。境界値は、0、1、最大値、最小値、期限日当日など、条件が切り替わる境目を確認します。
何をテストすべきか迷ったら、「この処理が間違っていたら困るか」「仕様として明確に残したいか」「過去にバグが起きたか」を基準にするとよいでしょう。
7-2. テストコードが複雑になりすぎる
ユニットテストを書き始めると、テストコード自体が複雑になってしまうことがあります。
原因としては、テスト対象のクラスが多くの依存関係を持っている、1つのメソッドが多くの責務を持っている、テストデータの準備が長すぎる、モックの設定が増えすぎるなどが考えられます。
テストコードが複雑な場合、テスト対象の設計にも問題があるかもしれません。クラスの責務を分ける、依存関係を減らす、入力と出力を明確にするなど、テストしやすい設計を意識しましょう。
また、テスト内の共通処理はヘルパーメソッドやファクトリメソッドに切り出すと読みやすくなります。
C#private static User CreateActiveUser()
{
return new User
{
Id = 1,
Name = "山田太郎",
IsActive = true
};
}
ただし、共通化しすぎると逆にテストの前提が見えにくくなります。テストコードでは、多少の重複よりも読みやすさを優先した方がよい場面もあります。
7-3. テストが失敗する原因を特定できない
テストが失敗したときに原因を特定できない場合、テストの書き方を見直す必要があります。
よくある原因は、1つのテストで多くのことを確認しすぎていることです。複数の処理をまとめて検証すると、どの条件で失敗したのかがわかりにくくなります。
また、テスト名が曖昧だと、失敗時に何の動作が壊れたのか判断しづらくなります。Test1やCheckUserのような名前ではなく、期待する動作がわかる名前にしましょう。
失敗原因を特定しやすくするには、次の点を意識します。
テスト名に条件と期待結果を書く。1つのテストで1つの観点を確認する。Assertを複雑にしすぎない。テストデータをわかりやすくする。失敗メッセージを読んで期待値と実際の値を確認する。
テストが失敗すること自体は悪いことではありません。失敗によって問題を早く発見できるのがユニットテストの価値です。大切なのは、失敗したときに原因を素早く理解できるテストを書くことです。
7-4. テストデータの準備が大変になる
C#ユニットテストでは、テストデータの準備が大変になることがあります。特に、プロパティの多いクラスや複雑なオブジェクトを扱う場合、毎回長い初期化コードを書くのは負担になります。
たとえば、ユーザー情報、注文情報、商品情報などを毎回手で作ると、テストの本質が見えにくくなります。
このような場合は、テストデータ作成用のメソッドを用意すると便利です。
C#private static Order CreateOrder(int totalAmount)
{
return new Order
{
Id = 1,
TotalAmount = totalAmount,
Status = OrderStatus.Created
};
}
さらに複雑な場合は、テストデータビルダーを使う方法もあります。
C#public class UserBuilder
{
private string _name = "山田太郎";
private bool _isActive = true;
public UserBuilder WithName(string name)
{
_name = name;
return this;
}
public UserBuilder Inactive()
{
_isActive = false;
return this;
}
public User Build()
{
return new User
{
Name = _name,
IsActive = _isActive
};
}
}
テストデータは、必要な項目だけが目立つように準備するのが理想です。テストに関係ない値が多すぎると、読み手が何を確認しているのかわからなくなります。
7-5. モックの使いすぎでテストが壊れやすくなる
モックは便利ですが、使いすぎるとテストが壊れやすくなります。
特に、内部メソッドの呼び出し回数や呼び出し順序を細かく検証しすぎると、実装を少し変えただけでテストが失敗することがあります。外部から見た動作は変わっていないのに、内部構造を変更しただけでテストが壊れる状態は望ましくありません。
ユニットテストで重視すべきなのは、基本的に「入力に対して期待する結果になるか」です。モックのVerifyは、メール送信、ログ出力、外部通知など、戻り値だけでは確認できない副作用を検証したい場合に使うとよいでしょう。
また、モックが多く必要になるクラスは、依存関係が多すぎる可能性があります。設計を見直し、責務を分割できないか検討することも大切です。
モックは外部依存を切り離すための道具です。テスト対象の実装詳細を固定するための道具にならないよう注意しましょう。
8. 良いC#ユニットテストを書くためのベストプラクティス
8-1. テスト名は期待する動作がわかるように書く
良いユニットテストは、テスト名を見ただけで何を確認しているかわかります。
たとえば、次のような名前は避けた方がよいです。
C#public void Test1()
public void AddTest()
public void UserCheck()
これでは、どの条件で何を期待しているのかがわかりません。
代わりに、次のような名前にすると意図が伝わりやすくなります。
C#public void Add_2と3を渡すと5を返す()
public void GetDiscountRate_Gold会員なら20パーセントを返す()
public void Register_メールアドレスが空なら例外を投げる()
テスト名には、「対象メソッド」「条件」「期待結果」を含めると読みやすくなります。
日本語のメソッド名を使うか英語にするかはチーム方針によります。初心者の学習段階では、日本語で期待動作を書くと理解しやすいです。実務では、チームで命名ルールを統一しましょう。
8-2. 1つのテストで1つの観点を確認する
1つのテストでは、できるだけ1つの観点を確認するようにしましょう。
たとえば、ユーザー登録処理で「ユーザーが保存される」「確認メールが送信される」「初期ポイントが付与される」を1つのテストにまとめると、失敗したときに原因がわかりにくくなります。
それぞれの観点を別のテストに分けると、どの仕様が壊れたのかが明確になります。
ただし、Assertを必ず1つだけにしなければならないという意味ではありません。同じ観点を確認するために複数のプロパティを確認することはあります。たとえば、作成されたユーザーの名前とメールアドレスを同時に確認するのは自然な場合もあります。
重要なのは、テストの目的が1つに絞られていることです。テスト名で表した期待動作とAssertの内容が対応しているかを確認しましょう。
8-3. 外部依存を切り離して高速に実行できるようにする
良いユニットテストは高速に実行できます。開発中に何度も実行するため、1回の実行に時間がかかると使われなくなってしまいます。
テストが遅くなる主な原因は、データベース、外部API、ファイル、ネットワーク、現在時刻、ランダム値などへの依存です。
これらを直接使うと、テストが遅くなるだけでなく、環境によって結果が変わることがあります。ユニットテストでは、外部依存をインターフェースで抽象化し、モック、スタブ、フェイクに置き換えましょう。
たとえば、現在時刻を直接DateTime.Nowで取得するのではなく、IClockのようなインターフェースを経由すると、テスト時に固定時刻を使えます。
高速で安定したテストは、開発者が気軽に実行できます。その結果、バグの早期発見につながります。
8-4. テストしやすい設計にする
ユニットテストを書きやすいコードは、多くの場合、設計もシンプルです。
テストしにくいコードには、いくつかの特徴があります。1つのクラスが多くの責務を持っている、コンストラクタ内で外部接続を開始する、staticメソッドに依存しすぎている、DateTime.NowやGuid.NewGuid()を直接使っている、メソッド内で依存クラスをnewしている、などです。
テストしやすくするには、依存関係を外から渡す設計にします。これは依存性注入と呼ばれます。
C#public class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
}
このようにすると、テスト時にIOrderRepositoryをモックやフェイクに差し替えられます。
テストしやすい設計は、変更にも強くなります。ユニットテストを書くことは、コード設計を見直すきっかけにもなります。
8-5. カバレッジだけを目的にしない
テストカバレッジは、コードのうちどれだけがテストで実行されたかを示す指標です。C#開発でも、カバレッジを品質指標として使うことがあります。
しかし、カバレッジだけを目的にすると、意味の薄いテストが増える危険があります。
たとえば、メソッドを呼び出すだけでAssertがないテストや、実装に合わせて期待値を書いているだけのテストは、カバレッジが上がっても品質向上にはつながりにくいです。
大切なのは、「重要な仕様がテストで守られているか」です。カバレッジは参考にはなりますが、目的ではありません。
特に、金額計算、権限、契約状態、在庫、期限、セキュリティに関わる処理など、間違えると影響が大きい部分を優先的にテストしましょう。
良いユニットテストは、数字を満たすためではなく、安心して変更するために存在します。
9. C#ユニットテストを開発現場で活用する方法
9-1. リファクタリング時の安全網として使う
ユニットテストは、リファクタリング時に大きな効果を発揮します。
リファクタリングとは、外部から見た動作を変えずに、内部のコード構造を改善することです。たとえば、長いメソッドを分割する、重複コードを共通化する、クラスの責務を整理する、といった作業です。
ユニットテストがない状態でリファクタリングを行うと、意図せず動作を変えてしまっても気づきにくいです。手動確認だけでは、すべてのパターンを毎回確認するのは大変です。
ユニットテストがあれば、リファクタリング前後でテストを実行し、結果が変わらないことを確認できます。テストが通っていれば、少なくともテストで守られている仕様は壊れていないと判断できます。
特に、複雑な条件分岐や業務ルールを整理する場合は、先にテストを書いてからリファクタリングすると安全です。
9-2. バグ修正時に再発防止テストを書く
バグを修正するときは、修正コードだけでなく再発防止テストも書くのがおすすめです。
まず、発生したバグを再現するテストを書きます。この時点では、まだ修正していないためテストは失敗するはずです。次に、コードを修正してテストが成功するようにします。
この流れを踏むことで、同じバグが将来再発したときにテストが検知してくれます。
たとえば、割引計算で特定の会員ランクだけ計算ミスがあった場合、その会員ランクのテストケースを追加します。
C#[Fact]
public void CalculateDiscount_Premium会員の場合は30パーセント割引する()
{
var service = new DiscountService();
var result = service.CalculateDiscount(1000, "Premium");
Assert.Equal(300, result);
}
バグは、仕様の見落としや境界条件の不足から発生することが多いです。バグ修正時にテストを追加することで、テストスイートが少しずつ強くなっていきます。
9-3. CI/CDで自動実行する
C#ユニットテストは、CI/CDに組み込むことでより効果を発揮します。
CIとは、コードを変更するたびにビルドやテストを自動実行する仕組みです。CDは、テストに通ったコードを自動でデプロイする仕組みです。
たとえば、GitHub Actionsでは、プルリクエスト作成時にdotnet testを実行できます。テストが失敗した場合は、マージを止める運用にすることも可能です。
基本的な流れは次の通りです。
コードをプッシュする。CIが自動でビルドする。CIがdotnet testを実行する。テストが成功すればマージやデプロイに進む。失敗すれば修正する。
CI/CDでユニットテストを自動実行すると、個人の確認漏れに依存しない品質管理ができます。チーム開発では特に重要です。
9-4. チーム開発でテスト方針を統一する
チームでC#ユニットテストを活用するには、テスト方針を統一することが大切です。
たとえば、使用するテストフレームワーク、テストクラスの命名規則、テストメソッドの命名規則、モックライブラリ、テストデータの作り方、どの範囲までユニットテストで確認するか、カバレッジの目安などを決めておきます。
方針がバラバラだと、テストコードの書き方が人によって異なり、保守しにくくなります。逆に、基本ルールが統一されていると、他の人のテストも読みやすくなります。
ただし、最初から細かく決めすぎる必要はありません。まずは最低限のルールを決め、運用しながら改善していくのが現実的です。
たとえば、最初は次のようなルールで十分です。
テストプロジェクト名はプロジェクト名.Testsにする。テストクラス名は対象クラス名Testsにする。テストメソッド名は条件と期待結果がわかる名前にする。外部APIやDBに依存するものはユニットテストに含めない。バグ修正時は再発防止テストを書く。
チームで共通認識を持つことで、ユニットテストが個人任せではなく開発文化として定着しやすくなります。
9-5. レガシーコードにユニットテストを追加する手順
既存のレガシーコードにユニットテストを追加するのは簡単ではありません。依存関係が複雑だったり、staticメソッドが多かったり、外部DBに直接接続していたりするためです。
いきなり全体をテストしようとすると挫折しやすいので、影響の大きい部分から少しずつ始めましょう。
おすすめの手順は次の通りです。
まず、変更予定のある箇所やバグが多い箇所を選びます。次に、現在の動作を確認するためのテストを書きます。この時点では、理想の仕様ではなく「現状の動作」を固定することを目的にします。その後、安全にリファクタリングし、必要に応じて設計を改善します。
外部依存が強い場合は、インターフェースを導入して依存を切り離します。ただし、大規模に書き換えるのではなく、小さな変更を積み重ねることが大切です。
レガシーコードでは、「完璧なテストを一気に作る」よりも、「次に変更する部分を少しずつ守る」方が現実的です。バグ修正や機能追加のたびにテストを増やしていくと、徐々に安全網が広がっていきます。
10. C#ユニットテストに関するよくある質問
10-1. ユニットテストはどこまで書くべき?
ユニットテストは、すべてのコードに対して機械的に書けばよいわけではありません。重要なのは、壊れると困るロジックを優先的にテストすることです。
金額計算、権限判定、入力値検証、業務ルール、状態遷移、日付判定などは、ユニットテストを書く価値が高い部分です。
一方で、単純なプロパティのgetter/setterや、フレームワークに任せているだけの薄いコードは、無理にテストしなくてもよい場合があります。
目安としては、「その処理が間違っていたらユーザーや業務に影響があるか」「今後変更される可能性があるか」「過去にバグが起きたか」を考えると判断しやすいです。
10-2. すべてのメソッドにテストは必要?
すべてのメソッドにユニットテストが必要とは限りません。
特に、privateメソッドや単純なラッパーメソッドまで直接テストしようとすると、テストの数が増えすぎて保守が大変になります。
ユニットテストでは、メソッド単位というより「外部から見た振る舞い」を確認することが大切です。publicメソッドを通じて重要な動作が確認できていれば、内部のprivateメソッドを個別にテストする必要はありません。
ただし、publicメソッドであっても、重要なロジックを持っている場合はテストを書いた方がよいです。逆に、単純に別のメソッドを呼び出しているだけなら、優先度は下がります。
10-3. テストコードにもリファクタリングは必要?
テストコードにもリファクタリングは必要です。
テストコードは本番コードと同じように、長期間保守されるコードです。読みづらいテスト、重複が多すぎるテスト、意図がわからないテストは、時間が経つほど開発の負担になります。
ただし、テストコードのリファクタリングでは、共通化しすぎに注意が必要です。テストの準備処理を抽象化しすぎると、各テストが何を前提にしているのかわかりにくくなります。
テストコードでは、読みやすさを最優先にしましょう。多少の重複があっても、テストの意図が明確な方が保守しやすい場合があります。
10-4. MSTest・NUnit・xUnitは混在できる?
技術的には、MSTest、NUnit、xUnitを同じソリューション内で混在させることは可能です。別々のテストプロジェクトに分ければ、それぞれのフレームワークでテストを実行できます。
しかし、特別な理由がない限り、混在はおすすめしません。フレームワークごとに属性、Assert、セットアップ方法、パラメータ化テストの書き方が異なるため、テストコードの統一感がなくなります。
チーム開発では、基本的に1つのフレームワークに統一する方が保守しやすいです。
既存プロジェクトで複数のフレームワークが混在している場合は、無理に一括移行するのではなく、新規テストはどれで書くかを決め、必要に応じて段階的に整理するとよいでしょう。
10-5. 初心者が最初に覚えるべき内容は?
C#ユニットテスト初心者が最初に覚えるべき内容は、テストフレームワークの細かい機能ではなく、基本的な考え方です。
まずは、AAAパターンを理解しましょう。準備、実行、検証の3つに分けて書くことで、読みやすいテストになります。
次に、Assertの基本を覚えます。戻り値が期待通りか、trueまたはfalseになるか、nullかどうか、例外が発生するかを確認できれば、基本的なテストは書けます。
その後、複数パターンのテスト、例外のテスト、モックの使い方を学ぶとよいです。
最初から完璧なテストを書こうとする必要はありません。まずは小さなメソッドに対して、入力と期待結果が明確なテストを書くことから始めましょう。
まとめ
C#ユニットテストは、メソッドやクラスなど小さな単位の動作を自動で確認するための仕組みです。テストを書いておくことで、バグの早期発見、リファクタリング時の安全性向上、仕様の明確化、チーム開発での品質維持に役立ちます。
初心者は、まずユニットテストの目的を理解し、AAAパターンに沿ってシンプルなテストを書くことから始めるとよいでしょう。テスト対象を準備し、メソッドを実行し、Assertで期待結果を確認する。この基本がC#ユニットテストの土台になります。
フレームワークは、MSTest、NUnit、xUnitのいずれを選んでも基本的な考え方は同じです。Visual Studio中心ならMSTest、柔軟な書き方を重視するならNUnit、シンプルでモダンな書き方を重視するならxUnitが候補になります。迷った場合は、チームの方針や既存プロジェクトに合わせるのが最も実用的です。
実践では、戻り値のあるメソッド、条件分岐、例外、日付・時刻、外部依存を含む処理などを順番にテストしていきます。外部API、DB、ファイル操作などは直接使わず、インターフェースやモックを活用して、速く安定したユニットテストにすることが大切です。
また、良いユニットテストを書くには、テスト名をわかりやすくし、1つのテストで1つの観点を確認し、外部依存を切り離し、テストしやすい設計を意識する必要があります。カバレッジだけを目的にせず、重要な仕様を守るためにテストを書くことが重要です。
C#ユニットテストは、最初は難しく感じるかもしれません。しかし、小さなテストを1つずつ書いていくことで、コードの品質と開発の安心感は確実に高まります。まずは簡単なメソッドからテストを書き、日々の開発に少しずつ取り入れていきましょう。

