C#のテストで例外を検証する方法を徹底解説!デバッグとエラー対策の基本
生徒
「C#でプログラムを作っているとき、わざとエラーが起きるか確認したい場合はどうすればいいですか?」
先生
「それは例外テストという手法を使います。想定外の入力があったときに、正しくエラーを投げられるかをチェックする大切な作業です。」
生徒
「エラーが起きるのが正解、ということもあるんですね。具体的なやり方を教えてください!」
先生
「もちろんです。テストフレームワークを使って、安全に例外を検証する方法を学んでいきましょう!」
1. 例外とは何かを学ぼう
プログラミングの世界では、予期せぬトラブルのことを例外(れいがい)と呼びます。例えば、数字をゼロで割ろうとしたり、存在しないファイルを読み込もうとしたりしたときに、コンピューターは「これ以上処理を続けられません!」と悲鳴をあげます。これが例外が発生した状態です。
初心者のうちは、例外が出ると「失敗した」と思いがちですが、実はそうではありません。プログラムが暴走したり、間違ったデータを保存したりする前に、安全に停止してくれる防御機能なのです。C#では、この例外を意図的に発生させたり、逆に発生することをテストで確認したりすることが非常に重要になります。
2. テストで例外を検証する目的
なぜ、わざわざエラーが起きることを確認する必要があるのでしょうか。それは、あなたの作ったプログラムが「悪い操作に対して適切に反応できるか」を確かめるためです。これを異常系テストと呼びます。
例えば、銀行の引き出しシステムを想像してください。残高が千円しかないのに、一万円を引き出そうとしたら、システムは「残高不足です」というエラーを出すべきです。もしエラーが出ずに処理が進んでしまったら大変なことになりますよね。このように、「ダメなものはダメ」と正しく判断できているかをチェックするのが、例外の検証作業なのです。
3. ユニットテストの基本準備
C#でテストを行うには、通常xUnitやNUnitといった「テストフレームワーク」という道具を使います。これは、プログラムが正しく動くか自動で判定してくれる便利なツールです。今回は、特によく使われるxUnitというツールを前提に解説します。
テストを書くときは、まず「何をテストするか」を決めます。テストの対象となるメソッド(処理のまとまり)を呼び出し、その結果が自分の予想通りになるかを比較します。例外のテストの場合は、「この命令を実行したら、特定の名前のエラーが発生すること」を予想として設定します。
4. Assert.Throwsを使った基本的な検証
xUnitで例外を検証する最も一般的な方法は、Assert.Throwsという命令を使うことです。これは「これから実行する処理で、必ずこのエラーを投げなさい」とコンピューターに命令する書き方です。
まずは、簡単な割り算のプログラムを例に見てみましょう。ゼロで割ることは数学的にできないため、エラーが発生するはずです。
using Xunit;
public class MathChecker
{
public int Divide(int left, int right)
{
if (right == 0)
{
throw new System.DivideByZeroException("ゼロで割ることはできません");
}
return left / right;
}
}
public class MathTests
{
[Fact]
public void ZeroDivisionTest()
{
var checker = new MathChecker();
// 0で割ったときに、DivideByZeroExceptionという例外が発生するか検証します
Assert.Throws<System.DivideByZeroException>(() => checker.Divide(10, 0));
}
}
このコードでは、Divideメソッドの引数に「0」を渡したとき、正しくエラーが発生するかをテストしています。もしエラーが起きなければ、このテストは「失敗」となり、プログラムに不備があることがわかります。
5. 例外のメッセージ内容を確認する
ただエラーが出るだけでなく、エラーメッセージの内容が正しいかどうかをチェックしたい場合もあります。例えば「入力が空です」と出るべきなのに「システムエラー」と出たら、利用者は困ってしまいますよね。
Assert.Throwsは、発生した例外そのものを戻り値として返してくれるので、その中身を詳しく調べることが可能です。次の例を見てみましょう。
[Fact]
public void MessageCheckTest()
{
var checker = new MathChecker();
// 発生した例外を変数「ex」に受け取ります
var ex = Assert.Throws<System.DivideByZeroException>(() => checker.Divide(10, 0));
// エラーメッセージが期待通りか確認します
Assert.Equal("ゼロで割ることはできません", ex.Message);
}
このように書くことで、単に種類が合っているかだけでなく、ユーザーに伝える言葉まで正確にテストできるようになります。これを丁寧に行うことで、品質の高いプログラムへと近づきます。
6. 複数の条件で例外をテストする方法
一つの処理に対して、色々なパターンの悪いデータを入れて試したいことがあります。これをデータ駆動テストと言います。C#のテストでは、[Theory]と[InlineData]という機能を使って、効率よくテストを回せます。
例えば、名前を入力する欄があったとして、「名前が短すぎる場合」や「名前が空っぽの場合」にエラーが出るかまとめて確認してみましょう。
public class UserRegistration
{
public void Register(string name)
{
if (string.IsNullOrEmpty(name))
{
throw new System.ArgumentException("名前を入力してください");
}
if (name.Length < 2)
{
throw new System.ArgumentException("名前は2文字以上にしてください");
}
}
}
public class RegistrationTests
{
[Theory]
[InlineData(null)] // データが空の場合
[InlineData("")] // 空文字の場合
[InlineData("あ")] // 1文字だけの場合
public void InvalidNameTest(string targetName)
{
var reg = new UserRegistration();
// どのデータが来ても、ArgumentExceptionが発生することを期待します
Assert.Throws<System.ArgumentException>(() => reg.Register(targetName));
}
}
この書き方をすると、一つのテストプログラムで3つのパターンを自動的に実行してくれます。何度も似たようなコードを書かなくて済むので、非常にスマートです。
7. 非同期処理での例外テスト
最近のプログラミングでは、インターネットからデータを取ってくるような、待ち時間が発生する処理(非同期処理)がよく使われます。C#ではasyncやawaitというキーワードを使いますが、この場合の例外テストは少しだけ書き方が異なります。
非同期の場合はAssert.ThrowsAsyncという専用の命令を使います。これを知らないと、テストが正しく終わる前に判定が下されてしまい、うまく検証できないので注意しましょう。
public class FileLoader
{
public async System.Threading.Tasks.Task LoadAsync(string path)
{
await System.Threading.Tasks.Task.Delay(10); // 少し待つ処理
if (path == null)
{
throw new System.ArgumentNullException();
}
}
}
public class LoaderTests
{
[Fact]
public async System.Threading.Tasks.Task AsyncExceptionTest()
{
var loader = new FileLoader();
// 非同期の例外待機には ThrowsAsync を使います
await Assert.ThrowsAsync<System.ArgumentNullException>(() => loader.LoadAsync(null));
}
}
普通のテストと似ていますが、awaitを付けることと、ThrowsAsyncを使うことがポイントです。これで最新のプログラム形式にもバッチリ対応できます。
8. デバッグ機能で例外の原因を突き止める
テストで例外が発生することを確認できたら、次は「なぜその例外が起きたのか」を詳しく調べるデバッグの出番です。デバッグとは、プログラムの中身を一行ずつ覗き見して、犯人探しをすることです。
Visual Studioなどの開発ツールには、例外が発生した瞬間にプログラムを一時停止させる機能があります。停止した場所で、その時の変数の値(中身のデータ)をマウスで指し示すと、何が原因でエラーになったのかが一目瞭然です。テストコードを動かしながら、このデバッグ機能を併用することで、バグを修正するスピードが劇的に上がります。
9. 初心者が陥りやすい例外テストの罠
例外のテストを書くときに、一番やってはいけないのが「広すぎる範囲でテストすること」です。例えば、Assert.Throws<Exception>のように、一番大元の大雑把なエラーを指定してしまうと、どんなエラーが起きても合格してしまいます。
「ゼロで割ったエラー」を期待しているのに、「ファイルが見つからないエラー」が起きても合格になってしまったら、それは正しいテストとは言えません。できるだけ具体的で細かいエラーの種類(型)を指定するように心がけましょう。これにより、意図しない場所でバグが起きていることに気づきやすくなります。
10. テストを書く習慣を身につけよう
プログラミングを始めたばかりの頃は、動くものを作るだけで精一杯かもしれません。しかし、後から「これ、変なデータ入れたら壊れるかな?」と不安になることは多いです。そんなときに、今回学んだ例外テストを一つ書いておくだけで、その不安は解消されます。
プログラムを修正したときに、過去に作った機能が壊れていないか一瞬で確認できるのもテストの魅力です。初心者だからこそ、最初から少しずつテストを書く練習をしてみてください。半年後の自分に感謝されること間違いなしです!