search
LoginSignup
4
Help us understand the problem. What are the problem?

posted at

updated at

xUnitでExceptionをテストする

はじめに

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を使用する方法は、見た目も動作的にも一番しっくりくるように思いました。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
4
Help us understand the problem. What are the problem?