[C#]Throwing Exceptions with Moq when a Method is Called

目次

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 Moq must be installed.
    • Implementation should be tested using a try-catch block to verify the exception.

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, use ThrowsAsync<Exception>().

Points of Caution

  • Generic vs. Instance Specification: Throws<ArgumentException>() internally creates and throws a new ArgumentException() using the parameterless constructor. If you need to include messages or specific parameters (like ParamName), you must use the instance format: new ArgumentException(...).
  • Execution Order and Overlapping Matches: If multiple Setup calls match the arguments, Moq usually prioritizes the last defined or most specific setup. However, it is best to keep your It.Is conditions 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 a MockException. 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.

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次