C#で業務アプリを作るなら、 単体テスト(ユニットテスト) を書いておくと、 改修・リファクタリング・バグ修正のたびに安心感がまったく違います。 この記事では xUnit / MSTest / Moq を軸に、 現場で使うテストの“実務パターン”をまとめます。
この記事でわかること
・xUnit / MSTest の違いと選び方
・テストプロジェクトの作り方
・基本的なテストの書き方
・Moqで依存オブジェクトをモックする方法
・非同期メソッドのテスト
・例外テスト・境界値テスト
・サービス層・リポジトリ層のテストパターン
・xUnit / MSTest の違いと選び方
・テストプロジェクトの作り方
・基本的なテストの書き方
・Moqで依存オブジェクトをモックする方法
・非同期メソッドのテスト
・例外テスト・境界値テスト
・サービス層・リポジトリ層のテストパターン
1. xUnit と MSTest のざっくり比較
| 項目 | xUnit | MSTest |
|---|---|---|
| 属性 | [Fact] / [Theory] | [TestMethod] / [DataTestMethod] |
| 人気・情報量 | ◎(OSS界隈で主流) | ○(Microsoft公式) |
| 特徴 | シンプル・拡張性高い | Visual Studioとの親和性◎ |
どちらでもOK ですが、 新規なら xUnit を選ぶケースが増えています。
2. テストプロジェクトの作り方
■ 2-1. xUnit の場合
dotnet new xunit -n MyApp.Tests
dotnet add MyApp.Tests reference MyApp
■ 2-2. MSTest の場合
dotnet new mstest -n MyApp.Tests
dotnet add MyApp.Tests reference MyApp
テストプロジェクトから、テスト対象プロジェクト(本体)に参照を追加します。
3. xUnit の基本([Fact] / [Theory])
■ 3-1. 単純なテスト([Fact])
public class CalculatorTests
{
[Fact]
public void Add_1_and_2_returns_3()
{
var calc = new Calculator();
var result = calc.Add(1, 2);
Assert.Equal(3, result);
}
}
■ 3-2. パラメータ付きテスト([Theory])
public class CalculatorTheoryTests
{
[Theory]
[InlineData(1, 2, 3)]
[InlineData(10, 5, 15)]
[InlineData(-1, 1, 0)]
public void Add_returns_expected(int a, int b, int expected)
{
var calc = new Calculator();
var result = calc.Add(a, b);
Assert.Equal(expected, result);
}
}
同じロジックを複数パターンで検証できます。
4. MSTest の基本([TestMethod])
■ 4-1. 単純なテスト
[TestClass]
public class CalculatorTests
{
[TestMethod]
public void Add_1_and_2_returns_3()
{
var calc = new Calculator();
var result = calc.Add(1, 2);
Assert.AreEqual(3, result);
}
}
■ 4-2. データ駆動テスト
[DataTestMethod]
[DataRow(1, 2, 3)]
[DataRow(10, 5, 15)]
public void Add_returns_expected(int a, int b, int expected)
{
var calc = new Calculator();
var result = calc.Add(a, b);
Assert.AreEqual(expected, result);
}
5. Moq で依存オブジェクトをモックする
■ 5-1. パッケージ追加
dotnet add MyApp.Tests package Moq
■ 5-2. サービスがリポジトリに依存している例
public interface IUserRepository
{
Task<User?> FindAsync(int id);
}
public class UserService
{
private readonly IUserRepository _repo;
public UserService(IUserRepository repo) { _repo = repo; }
public async Task<string> GetUserNameAsync(int id)
{
var user = await _repo.FindAsync(id);
return user?.Name ?? "不明";
}
}
■ 5-3. Moqでリポジトリをモックしてテスト
public class UserServiceTests
{
[Fact]
public async Task GetUserNameAsync_returns_name_when_found()
{
var mock = new Mock<IUserRepository>();
mock.Setup(x => x.FindAsync(1))
.ReturnsAsync(new User(1, "Taro"));
var service = new UserService(mock.Object);
var name = await service.GetUserNameAsync(1);
Assert.Equal("Taro", name);
}
}
DBに接続せず、サービスのロジックだけを検証できます。
6. 非同期メソッドのテスト
■ 6-1. async Task をそのままテスト
[Fact]
public async Task LoadAsync_sets_flag()
{
var vm = new MyViewModel();
await vm.LoadAsync();
Assert.True(vm.IsLoaded);
}
テストメソッドも async Task にするのがポイントです。
7. 例外テスト(エラーケースの検証)
■ 7-1. xUnit の場合
[Fact]
public void Create_throws_when_amount_is_negative()
{
Assert.Throws<ArgumentException>(() =>
{
new Order(-1);
});
}
■ 7-2. MSTest の場合
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void Create_throws_when_amount_is_negative()
{
new Order(-1);
}
業務アプリでは「エラー時に例外を投げる」パターンも必ずテストしておきます。
8. サービス層のテストパターン
■ 8-1. 正常系+異常系をセットで書く
- 正常に保存できる
- 入力が不正なら例外を投げる
- リポジトリが null を返したときの挙動
■ 8-2. 依存先はすべてモック
- リポジトリ(DB)
- 外部APIクライアント
- メール送信クラス
「外部と通信するもの」はすべてモックにして、 サービスのロジックだけを検証します。
9. テストしやすい設計とのセット運用
- new せず DI(コンストラクタ注入)にする
- static メソッドを減らす
- ビジネスロジックをUIから切り離す
テストしやすい構造=保守しやすい構造です。
10. 業務アプリ向けベストプラクティス
- 新規なら xUnit + Moq が扱いやすい
- テストプロジェクトは本体と同じ名前空間構造にする
- サービス層を優先的にテスト対象にする
- DB・APIはモックし、ロジックだけ検証する
- 正常系+異常系(例外)をセットで書く
- CI(GitHub Actions / Azure DevOps)で自動実行する
まとめ:C#のテストは“xUnit / MSTest × Moq × DI”で一気に実務レベルになる
- テストフレームワークは xUnit / MSTest どちらでもOK
- Moqで依存をモックすると、サービスロジックを安全に検証できる
- 非同期・例外・境界値を押さえるとバグが激減する
「テストを書きたいけど、どこから手を付ければいいか分からない」 という状態でも、 この記事のパターンをそのまま真似すれば、 C# × テスト(xUnit / MSTest / Moq)を実務レベルで回せるようになります。