• 10 hours
  • Easy

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 2/6/20

Handle exceptions with tests and test suites

What happens as our app gets more complex?

Let's go back to our CalculatorProgram functionality.

Instead of declaring the code for the operations, first, make the test cases and then implement the functionality from there. As you may remember, you start with test-failing scenarios (red), then implement the tests and make them pass (green), and then refactor the code as needed.

Our next functionalities will include the divide and multiply operations. By thinking of the test cases first, you can identify the following:

  • In the context of our calculator program, a multiply operation will be supported to receive an array of numbers.

  • The same as the InMemory support feature, we can pass a new number to the multiply operation, and the calculator will keep the result in memory and available for subsequent operations.

  • The same features above will be supported for the divide operation, with special attention in the case of having divide by zero operations, which might result in an exception to our program.

Let's implement the multiply and divide operations using TDD. We will start by creating a new test class:

using System;
using CalculatorProgram;
using Xunit;

namespace UnitTests
{
public class MultiplyDivideTests
{
}
}

Then we add a new [Fact] called Test_MultiplyTwoElements and Test_MultiplyManyElements for expecting the results of the multiply operation over a set of numbers. In the case of the InMemory feature, we can have a separate test for accepting one number and expect the results to be stored, cleared, and so on.

[Fact]
public void Test_MultiplyTwoElements()
{
// 1) Arrange
var calculator = new Calculator();

// Act (the actual operation)
var result = calculator.Multiply(10, 5);

// Then, Assert
Assert.Equal(50, result);
}

[Fact]
public void Test_MultiplyManyElements()
{
// 1) Arrange
var calculator = new Calculator();

// Act (the actual operation)
var result = calculator.Multiply(0.5M, 1, 2, 3, 4, -5.5M);

// Then, Assert
Assert.Equal(-66, result);
Assert.IsType<decimal>(result);
}

The same applies to the divide operation, so our tests will end up like the following:

[Fact]
public void Test_DivideTwoElements()
{
// 1) Arrange
var calculator = new Calculator();

// Act (the actual operation)
var result = calculator.Divide(10, 5);

// Then, Assert
Assert.Equal(2, result);
}

[Fact]
public void Test_DivideManyElements()
{
// 1) Arrange
var calculator = new Calculator();

// Act (the actual operation)
var result = calculator.Divide(100, 2, 4);

// Then, Assert
Assert.Equal((decimal)12.5, result);
Assert.IsType<decimal>(result);
}

Since our code is not implemented, we can add both of the new methods and implement them by default as NotImplementedException, so they will make our test base fail:

Failing test cases
Failing test cases in Visual Studio

Now that we have added our test-failing cases using TDD, let's make the implementation so we can test the final code again and get some feedback:

public decimal Multiply(params decimal[] numbers)
{
if (numbers.Length == 1)
{
Current *= numbers[0];
return Current;
}

var result = numbers[0];
for (int i = 1; i <= numbers.Length - 1; i++)
{
result *= numbers[i];
}
return result;
}

public decimal Divide(params decimal[] numbers)
{
if (numbers.Length == 1)
{
Current /= numbers[0];
return Current;
}

var result = numbers[0];
for (int i = 1; i <= numbers.Length - 1; i++)
{
result /= numbers[i];
}
return result;
}
Passed test cases
Passed test cases in Visual Studio

Sweet! TDD in practice. 😉

Expect exceptions

Let's handle the divide by zero support for the calculator now starting from a single test: 

Screenshot of Divide-by-Zero turned red
The newly added divide by zero support fails the test!

Wait, why is that failing? 😳

Actually, that's to be expected. As mentioned previously, the Divide action method continues running even if users send a divide by zero operation. Instead of allowing our program to fail, we can make an exception support occur and handle the exception properly.

Handle exceptions with the Assert.Throws method

We can reflect this exception in our test cases by using the Assert.Throws\<T\> method, which accepts a generic type that can be used with the exception of our needs (i.e.,  Assert.Throws\<System.DivideByZeroException\>()  ). So our test code ends up like the following:

[Fact]
public void Test_DivideByZero()
{
// 1) Arrange
var calculator = new Calculator();

Assert.Throws<DivideByZeroException>(() => {

// Act (the actual operation)
var result = calculator.Divide(1, 0);

// Then, Assert
Assert.IsType<decimal>(result);
});
}

By making that we can see our test case becomes green now:

Divide-by-Zero passed
Divide by zero passed scenario

Much better! 😁

Handle multiple exception cases with a test suite

For cases where different kinds of exceptions may occur in your program, you can set multiple test cases from multiple classes. As mentioned previously, this combination of test cases is what's called a test suite (also, a validation suite). 

Test suites
A test suite is a combination of test cases from multiple test classes

Let's say that we want to perform certain validations in our CalculatorProgram for given parameters like decimal.MaxValue (in order to prevent a System.OverflowException ). We can do this by throwing an InvalidOperationException . We can have a dedicated test case to handle that:

[Fact]
public void Test_Validations()
{
// 1) Arrange
var calculator = new Calculator();

Exception exception = Assert.Throws<InvalidOperationException>(() =>
{
// Act (the actual operation)
var result = calculator.Multiply(decimal.MaxValue, decimal.MaxValue);

// Then, Assert
Assert.IsType<decimal>(result);
});

Assert.Equal("Not a valid number to use", exception.Message);

exception = Assert.Throws<DivideByZeroException>(() =>
{
// Act (the actual operation)
var result = calculator.Divide(decimal.MaxValue, 0);

// Then, Assert
Assert.IsType<decimal>(result);
});

Assert.IsType<DivideByZeroException>(exception);
}

Alternatively, we can check on the exception being thrown in the test case and perform any checks at the exception level to make sure it gets handled properly with enough details. Then, by having our test fail, we can make it green by providing the implementation:

public decimal Multiply(params decimal[] numbers)
{
ValidateNumbers(numbers);
if (numbers.Length == 1)
{
Current *= numbers[0];
return Current;
}

var result = numbers[0];
for (int i = 1; i <= numbers.Length - 1; i++)
{
result *= numbers[i];
}
return result;
}

private void ValidateNumbers(decimal[] numbers)
{
if (numbers.Any(number => number == decimal.MaxValue))
{
throw new InvalidOperationException("Not a valid number to use");
}
}

By making multiple exception cases in your code, you can handle scenarios where logic needs to be enforced.

Let's recap!

  • Identifying exceptions in your software features is important for dealing with non-happy path scenarios without having to wait for your software to be tested by final users.

  • With unit testing and xUnit for .NET, you can set those exceptions as part of your test cases and have specific code that makes sure they are handled as expected.

  • The Assert.Throws method is very useful for handling exceptions in your code.

Example of certificate of achievement
Example of certificate of achievement