Elegant Way to Verify That Method Throws Exception in .NET

From time to time, we face a situation when we need to define a variable that points to a method or an anonymous function. It could be a part of business logic or some test case from unit tests. C# has a particular type that allows pointing to a specific method with a particular list of parameters and return type. Instead of defining a new delegate type, C # contains build-in generic delegates such as Action and Func.

.net test exception is thrown

All code you can find in the repository.

Commonly used practice to validate input model with some business rules requires us to throw validation exception to indicate that model is invalid because input parameters are wrong.

Basic Validator

Let`s define the simplest business case that we accept input model with first name and name should not be null or empty. If the name violates the rule, we need to throw ValidationException.

Take a look at the input model:

1
2
3
class InputModel {
public string Name {get;set;}
}

It`s a good tone to define our own exception classes:

1
2
3
4
5
6
7
public class ValidationException : Exception
{
public ValidationException() {}
public ValidationException(string message) : base(message) { }
public ValidationException(string message, Exception ex) :
base(message, ex) { }
}

And the main player in this article, Validator with validation logic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Validator
{
public void Validate(InputModel model)
{
if (string.IsNullOrEmpty(model.Name)) {
throw new ValidationException("invalid name");
}
}
public Task ValidateAsync(InputModel model) {
if (string.IsNullOrEmpty(model.Name)) {
throw new ValidationException("invalid name");
}
return Task.CompletedTask;
}
}

How to cover exception with unit tests

Our test project utilizes NUnit as a test framework and FluentAssetrtions to make assertions clean and easily readable.

Let`s take a look at how we should cover such validation logic with unit tests.

At first, we will cover success flow when the model is valid. We need to be sure that no exception was thrown.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Test]
public void Validate_ShouldNotThrow_WhenModeIsValid()
{
// Arrange
var model = new InputModel
{
Name = "name"
};

// Act
Action validate = () => _validator.Validate(model);

// Assert
validate.Should().NotThrow();
}

In the example above, we define action validate and verify that this action doesn`t throw an exception during execution.
To verify that action throws exception, we do it in the same way:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[TestCase("")]
[TestCase(null)]
public void Validate_ShouldThrow_WhenInvalidName(string name)
{
// Arrange
var model = new InputModel
{
Name = name
};

// Act
Action validate = () => _validator.Validate(model);

// Assert
validate.Should().Throw<ValidationException>("invalid name");
}

Generic method Throw<T>(string message) helps to verify that exception has required type and exception message equals predefined expected exception message.

If we work with async methods, we need to change Action to Func<T>.

Take a look at the difference between Action and Func.

Async version of same tests looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[Test]
public void ValidateAsync_ShouldNotThrow_WhenModeIsValid()
{
// Arrange
var model = new InputModel
{
Name = "name"
};

// Act
Func<Task> validate = () => _validator.ValidateAsync(model);

// Assert
validate.Should().NotThrowAsync();
}

[TestCase("")]
[TestCase(null)]
public void ValidateAsync_ShouldThrow_WhenInvalidName(string name)
{
// Arrange
var model = new InputModel
{
Name = name
};

// Act
Func<Task> validate = () => _validator.ValidateAsync(model);

// Assert
validate.Should().ThrowAsync<ValidationException>("invalid name");
}

Conclusion

We always should verify all business logic in unit tests, and exceptions are part of business logic. As you see, it is pretty simple to test that method throws an exception, and it doesn`t matter method is synchronous or asynchronous.