はじめに
xUnitを使用してユニットテストを書いていくと、Exceptionをキャッチするテストを書くことがあります。
これまで、Assert.Throws を使うことが多かったのですが、腑に落ちにくいところがありました。
というのも、基本的に自分はテストはAAA (Arrange, Act, Assert)に分けて書くようにしていたのですが、このAssert.Throwsは果たしてどこに入るものなのか?という疑問がありました。
たとえばこれまでよくこんな感じで書いていました。
[Fact]
public void CustomerTest()
{
var exception = Assert.Throws<ArgumentException>(() =>
CreateCustomer(customerId, customerName, email));
Assert.Equal("名前が入力されていません", exception.Message);
}
Assert.Throwsは AAA (Arange, Act, Assert) のどこに入る?
一般的にAAAの基本構造は以下のようなものです
[Fact]
public void CustomerTest(){
// Arrange: 準備
// Act: SUT(テスト対象)へのテストを実施
// Assert: 結果の確認
}
では、Assert.Throwsは?
先ほどの例として挙げたコードを例にすると
[Fact]
public void CustomerTest()
{
// Arrange
string customerId = "id00001";
string customerName = "";
string email = "sample@example.com";
// Act
var exception = Assert.Throws<ArgumentException>(() =>
CreateCustomer(customerId, customerName, email));
// Assert
Assert.Equal("名前が入力されていません", exception.Message);
}
つまり、Assert.ThrowsがActに記述されることになります。
でも、"Assert"と書いているので、もやもやするんです。気にならない人もいるとは思いますが、Actと書きながらAssertがあるというのが自分としては、なんだか気持ちが悪いです。
Assert.ThrowsをAssertに書く方法
2つの方法を紹介させていただきます。
1. Actionを使用する
Actionを使って、メソッドだけをActに書き、Assert.Throwsの引数に使用するというやり方です。
こんな感じになります。
[Fact]
public void CustomerTest()
{
// Arrange
string customerId = "id00001";
string customerName = "";
string email = "sample@example.com";
// Act
void Action() => CreateCustomer(customerId, customerName, email);
// Assert
var exception = Assert.Throws<ArgumentException>(Action)
Assert.Equal("名前が入力されていません", exception.Message);
}
こんな風に、ActにはAction()を使用して分離させます。
ActにAction、AssertにAssert.Throwsが書かれていることになるので、見た感じでも分かりやすいように思います。
ちなみに戻り値のある場合は以下のようにFunction()を使用すればいいです。
// Act
Customer Function() => CreateCustomer(customer);
// Assert
var exception = Assert.Throws<ArgumentException>(Function)
2. Record.Exceptionを使用する方法
もう一つの方法はAssert.Throwsに似ているのですが、Exceptionを先に指定しない方法です。
ちなみにRecordというのはRecord型のことではなく、XUnitのRecordクラスです。
こんな感じになります
[Fact]
public void CustomerTest()
{
// Arrange
string customerId = "id00001";
string customerName = "";
string email = "sample@example.com";
// Act
var exception = Record.Exception(() => {
CreateCustomer(customerId, customerName, email);
}
// Assert
Assert.Equal(typeof(ArgumentNullException), exception.GetType());
Assert.Equal("名前が入力されていません", exception.Message);
}
Actはメソッドに集中し、Record.Exceptionで例外をキャッチして、Assertでその例外を確認するということができます。コードからも明確に例外を確認するテストであることが分かります。
まとめ
いずれの場合でもテスト自体は成功しますが、テストは「何をテストしているのか明瞭である」ことが大切だと思います。
1つ目のActionの使用はパッと見は分かるのですが、ただメソッドの宣言だけで実際に実行しているのはAssert内となっています。それに対し2つ目のRecord.ExceptionはActで実行をしています。
そう思うと、最後のRecord.Excptionを使用する方法は、見た目も動作的にも一番しっくりくるように思いました。