Overview
Confirming the behavior of “error handling” and “exception cases” is crucial in unit testing. Using the Moq library, you can intentionally throw an exception when a method is called. This allows you to safely test the application’s response to errors, such as logging, retry logic, or the display of error messages.
Specifications (Input/Output)
- Input:
- The interface to be mocked (e.g., an inventory management service).
- Argument conditions that serve as the trigger for the exception.
- Output: When the method is called under the specified conditions, the defined exception is thrown.
- Prerequisites:
- The NuGet package
Moqmust be installed. - Implementation should be tested using a
try-catchblock to verify the exception.
- The NuGet package
Basic Usage
Use .Throws<TException>() to throw an exception of a specific type.
var mock = new Mock<IPaymentGateway>();
// Throw an ArgumentNullException if the argument is null
mock.Setup(x => x.ProcessPayment(null))
.Throws<ArgumentNullException>();
Full Code
The following is a scenario simulating an “Inventory Service.” This code demonstrates throwing different exceptions based on argument conditions, such as an invalid ID or an incorrect quantity.
using System;
using Moq;
// Requires NuGet package "Moq"
// dotnet add package Moq
namespace MoqExceptionExample
{
// Interface to be tested: Inventory Service
public interface IInventoryService
{
// Deducts stock for a specific product
void RemoveStock(int productId, int quantity);
}
class Program
{
static void Main()
{
// Create the mock
var mock = new Mock<IInventoryService>();
// Setup 1: Throw ArgumentOutOfRangeException if quantity is 0 or less (type only)
mock.Setup(s => s.RemoveStock(It.IsAny<int>(), It.Is<int>(q => q <= 0)))
.Throws<ArgumentOutOfRangeException>();
// Setup 2: Throw InvalidOperationException with a specific message if productId is 999 (instance)
mock.Setup(s => s.RemoveStock(999, It.IsAny<int>()))
.Throws(new InvalidOperationException("Product ID: 999 is currently suspended."));
// Setup 3: Normal case (Explicitly verifying it does nothing for success)
mock.Setup(s => s.RemoveStock(1, 10)).Verifiable();
var service = mock.Object;
Console.WriteLine("--- Starting Tests ---");
// Case 1: Testing exception for invalid quantity
try
{
Console.WriteLine("1. Attempting to deduct with quantity -5...");
service.RemoveStock(1, -5);
}
catch (ArgumentOutOfRangeException)
{
Console.WriteLine("-> Success: ArgumentOutOfRangeException occurred as expected.");
}
// Case 2: Testing exception for a specific ID
try
{
Console.WriteLine("\n2. Attempting to deduct with forbidden ID (999)...");
service.RemoveStock(999, 10);
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"-> Success: Exception occurred as expected. Msg: {ex.Message}");
}
// Case 3: Testing a normal call
try
{
Console.WriteLine("\n3. Attempting with normal ID and quantity...");
service.RemoveStock(1, 10);
Console.WriteLine("-> Success: No exception occurred.");
}
catch (Exception ex)
{
Console.WriteLine($"-> Failure: An unexpected exception occurred: {ex}");
}
}
}
}
Output Example
--- Starting Tests ---
1. Attempting to deduct with quantity -5...
-> Success: ArgumentOutOfRangeException occurred as expected.
2. Attempting to deduct with forbidden ID (999)...
-> Success: Exception occurred as expected. Msg: Product ID: 999 is currently suspended.
3. Attempting with normal ID and quantity...
-> Success: No exception occurred.
Customization Points
- Validating Exception Messages: If you need to verify the specific content of an error message, pass an instance instead of just the type, like
Throws(new Exception("Message")). This allows for more strict testing. - Asynchronous Methods: For methods returning a
Task, useThrowsAsync<Exception>().
Points of Caution
- Generic vs. Instance Specification:
Throws<ArgumentException>()internally creates and throws anew ArgumentException()using the parameterless constructor. If you need to include messages or specific parameters (likeParamName), you must use the instance format:new ArgumentException(...). - Execution Order and Overlapping Matches: If multiple
Setupcalls match the arguments, Moq usually prioritizes the last defined or most specific setup. However, it is best to keep yourIt.Isconditions distinct to avoid confusion. - Strict Mode Behavior: If you use
MockBehavior.Strict, calling a method with arguments that have not been set up will result in aMockException. Be careful not to let this interfere with your intended exception tests.
Application
Testing Retry Logic (First Fail, Then Succeed)
To test retry logic, such as for a “temporary network error,” use SetupSequence to change behavior for each subsequent call.
// First call fails with an exception, second call succeeds
mock.SetupSequence(s => s.RemoveStock(It.IsAny<int>(), It.IsAny<int>()))
.Throws(new TimeoutException("Communication timeout"))
.Pass(); // Pass() indicates that a void method does nothing (success)
Summary
Use the Throws method in Moq to trigger exceptions. Choose the generic Throws<T>() for simple type checks, or the instance version Throws(new T(...)) when you need to verify error messages or specific properties. This increases the coverage of your error-handling flows and helps build more robust applications.
