Question

How to Assert Exceptions in MSTest (C#)

csharpunit-testingmstestassertvs-unit-testing-framework

Question

How can I use Assert or other testing features in MSTest (Microsoft.VisualStudio.TestTools.UnitTesting) to verify that a specific exception is thrown?

For example, I want to test code that should fail under certain conditions and confirm that the expected exception is raised during the test.

Short Answer

By the end of this page, you will understand how to test exceptions in MSTest using modern and older approaches. You will learn when to use Assert.ThrowsException<T>(), how it works, why it is usually preferred over attribute-based exception testing, and how to write clear, reliable exception tests in C#.

Concept

In unit testing, verifying exceptions is how you confirm that code fails correctly.

Sometimes a method is supposed to throw an exception when it receives invalid input, missing data, or an impossible state. A good test should check not only successful behavior, but also failure behavior.

In MSTest, the most common way to verify this is:

Assert.ThrowsException<ArgumentException>(() => SomeMethod());

This means:

  • Run the code inside the lambda
  • Expect an exception of type ArgumentException
  • Fail the test if no exception is thrown
  • Fail the test if a different exception type is thrown

This matters because exception tests help you:

  • validate input rules
  • protect against silent failures
  • document intended behavior
  • catch regressions when code changes

For example, if a method should reject a negative price, your test should prove that it throws an exception instead of returning bad data.

In modern MSTest, Assert.ThrowsException<T>() is usually the clearest and most precise choice. Older code may also use the [ExpectedException] attribute, but that approach is less flexible and usually less explicit.

Mental Model

Think of an exception test like a fire alarm drill.

You are not hoping the alarm goes off randomly. You deliberately trigger a known condition and verify that the correct alarm responds.

  • Your code under test = the building system
  • Invalid input or bad state = the smoke trigger
  • Expected exception = the correct alarm
  • The test = the inspector checking that the right thing happened

If no alarm goes off, something is wrong. If the wrong alarm goes off, something is also wrong.

That is exactly what exception assertions do in a unit test.

Take Quiz

Syntax and Examples

The most common MSTest syntax is:

Assert.ThrowsException<ExceptionType>(() =>
{
    // code that should throw
});

Basic example

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class CalculatorTests
{
    [TestMethod]
    public void Divide_ByZero_ThrowsDivideByZeroException()
    {
        var calculator = new Calculator();

        Assert.ThrowsException<DivideByZeroException>(() =>
        {
            calculator.Divide(10, 0);
        });
    }
}

public class Calculator
{
    public int Divide(int a, int b)
    {
        if (b == 0)
        {
            throw new DivideByZeroException();
        }

        return a / b;
    }
}

Why this works

  • Assert.ThrowsException<DivideByZeroException>() expects that exact exception type
  • The lambda contains the code being tested
  • If Divide(10, 0) throws DivideByZeroException, the test passes

Capturing the exception for extra checks

You can also store the returned exception and inspect it.

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class UserServiceTests
{
    [TestMethod]
    public void CreateUser_EmptyName_ThrowsArgumentException()
    {
        var service = new UserService();

        var ex = Assert.ThrowsException<ArgumentException>(() =>
        {
            service.CreateUser("");
        });

        Assert.AreEqual("Name cannot be empty.", ex.Message);
    }
}

public class UserService
{
    public void CreateUser(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            throw new ArgumentException("Name cannot be empty.");
        }
    }
}

This is useful when you want to verify:

  • the exception message
  • custom exception properties
  • parameter names in argument exceptions

Step by Step Execution

Consider this example:

Assert.ThrowsException<ArgumentNullException>(() =>
{
    SaveFile(null);
});

And the method:

public void SaveFile(string path)
{
    if (path == null)
    {
        throw new ArgumentNullException(nameof(path));
    }

    // save logic
}

Here is what happens step by step:

  1. The test starts running.
  2. Assert.ThrowsException<ArgumentNullException> prepares to execute the lambda.
  3. The lambda calls SaveFile(null).
  4. Inside SaveFile, the condition path == null is true.
  5. The method throws ArgumentNullException.
  6. MSTest catches that exception.
  7. MSTest checks whether the caught exception is exactly the expected type.
  8. Because it matches, the assertion passes.
  9. The test succeeds.

If SaveFile(null) did not throw anything, the test would fail.

If it threw ArgumentException instead of ArgumentNullException, the test would also fail.

Small traceable example

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class ParserTests
{
    [TestMethod]
    public void ParseAge_NegativeNumber_ThrowsArgumentOutOfRangeException()
    {
        var parser = new Parser();

        Assert.ThrowsException<ArgumentOutOfRangeException>(() =>
        {
            parser.ParseAge(-1);
        });
    }
}

public class Parser
{
    public int ParseAge(int age)
    {
        if (age < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(age), "Age cannot be negative.");
        }

        return age;
    }
}

Execution flow:

  • ParseAge(-1) is called
  • age < 0 is true
  • exception is thrown
  • MSTest confirms the type
  • test passes

Real World Use Cases

Exception assertions are common in real software because many methods must reject invalid states.

Common scenarios

  • Input validation
    • A registration method throws ArgumentException for invalid email input
  • Null checks
    • A service throws ArgumentNullException when a required dependency or parameter is missing
  • Business rules
    • An order service throws InvalidOperationException if checkout is attempted with an empty cart
  • File and I/O handling
    • A file loader throws FileNotFoundException for missing files
  • Parsing and conversion
    • A parser throws FormatException when input is not in the correct format
  • Security and authorization
    • A method throws UnauthorizedAccessException when a user lacks permission

Example from an API-related service

public void SetPageSize(int pageSize)
{
    if (pageSize <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(pageSize));
    }
}

Test:

[TestMethod]
public void SetPageSize_Zero_ThrowsArgumentOutOfRangeException()
{
    var config = new ApiConfig();

    Assert.ThrowsException<ArgumentOutOfRangeException>(() =>
    {
        config.SetPageSize(0);
    });
}
Take Quiz

Real Codebase Usage

In real projects, developers usually use exception assertions together with a few common patterns.

Guard clauses

Guard clauses fail early when input is invalid.

public void SendEmail(string address)
{
    if (string.IsNullOrWhiteSpace(address))
        throw new ArgumentException("Email address is required.", nameof(address));

    // continue
}

Test:

Assert.ThrowsException<ArgumentException>(() => service.SendEmail(""));

Early validation

Many services validate arguments at the start of the method.

public void UpdatePrice(decimal price)
{
    if (price < 0)
        throw new ArgumentOutOfRangeException(nameof(price));
}

Verifying exception details

Real code often checks more than just the exception type.

var ex = Assert.ThrowsException<ArgumentNullException>(() => repo.Load(null));
Assert.AreEqual("id", ex.ParamName);

Error handling boundaries

Teams often test that low-level exceptions are transformed into application-specific exceptions.

public void Import(string path)
{
    try
    {
        File.ReadAllText(path);
    }
    catch (Exception ex)
    {
        throw new InvalidOperationException("Import failed.", ex);
    }
}

Test:

var ex = Assert.ThrowsException<InvalidOperationException>(() => importer.Import("missing.txt"));
Assert.AreEqual("Import failed.", ex.Message);

Why inline assertions are preferred

Using Assert.ThrowsException<T>() keeps the expected failure close to the exact line that should fail. That makes tests easier to read and safer when setup code gets longer.

Common Mistakes

1. Putting too much code inside the exception assertion

If you wrap too much code, the test may pass for the wrong reason.

Less clear

Assert.ThrowsException<ArgumentException>(() =>
{
    var service = new UserService();
    service.Initialize();
    service.CreateUser("");
});

If Initialize() throws, the test may still pass incorrectly depending on the expected type.

Better

var service = new UserService();
service.Initialize();

Assert.ThrowsException<ArgumentException>(() =>
{
    service.CreateUser("");
});

2. Using the wrong exception type

Assert.ThrowsException<Exception>(() => service.CreateUser(""));

This is usually too broad. Prefer the most specific type.

Better

Assert.ThrowsException<ArgumentException>(() => service.CreateUser(""));

3. Using [ExpectedException] for complex tests

Older MSTest code often uses:

[ExpectedException(typeof(ArgumentException))]
[TestMethod]
public void CreateUser_EmptyName_Throws()
{
    var service = new UserService();
    service.CreateUser("");
}

This works, but it is less precise because the exception could come from any line in the test method.

4. Forgetting to actually execute the method inside the lambda

Broken example:

Assert.ThrowsException<ArgumentException>(() => service.CreateUser);

This does not call the method.

Correct version:

Assert.ThrowsException<ArgumentException>(() => service.CreateUser(""));

5. Manually using try/catch when it is not needed

You can write this:

try
{
    service.CreateUser("");
    Assert.Fail("Expected ArgumentException was not thrown.");
}
catch (ArgumentException)
{
}

But Assert.ThrowsException<T>() is shorter and clearer.

6. Asserting exact exception messages too aggressively

Messages can vary across frameworks or versions. If you must check details, prefer stable properties such as ParamName when available.

var ex = Assert.ThrowsException<ArgumentNullException>(() => repo.Load(null));
Assert.AreEqual("id", ex.ParamName);

Comparisons

Common ways to test exceptions in MSTest

ApproachExampleProsConsRecommended?
Assert.ThrowsException<T>()Assert.ThrowsException<ArgumentException>(() => DoWork());Clear, precise, can inspect returned exceptionSlightly more verbose than an attributeYes
[ExpectedException][ExpectedException(typeof(ArgumentException))]Simple for very small testsLess precise, exception can come from any lineUsually no
Manual try/catchtry { ... } catch (...) { ... }Full controlMore boilerplate, easier to get wrongOnly when needed

Assert.ThrowsException<T>() vs [ExpectedException]

FeatureAssert.ThrowsException<T>()[ExpectedException]
Exception location is clearYesNo
Can inspect exception objectYesNo
Keeps setup outside assertionYesLess naturally
Good for modern MSTest codeYesMostly legacy

When manual try/catch is still useful

Use manual try/catch when:

  • you need to assert multiple things during exception handling
  • you are testing custom flows around nested exceptions
  • your framework version does not support the helper you want

Example:

try
{
    processor.Run();
    Assert.Fail("Expected exception was not thrown.");
}
catch (CustomException ex)
{
    Assert.AreEqual(42, ex.ErrorCode);
    Assert.IsNotNull(ex.InnerException);
}
Take Quiz

Cheat Sheet

// Preferred MSTest pattern
Assert.ThrowsException<ArgumentException>(() => SomeMethod());

// Capture exception for more checks
var ex = Assert.ThrowsException<ArgumentNullException>(() => SomeMethod(null));
Assert.AreEqual("paramName", ex.ParamName);

Quick rules

  • Use Assert.ThrowsException<T>() for most exception tests
  • Keep only the failing call inside the lambda when possible
  • Prefer specific exception types over Exception
  • Capture the returned exception if you need to inspect it
  • Use [ExpectedException] mainly when maintaining older tests

Older MSTest pattern

[ExpectedException(typeof(ArgumentException))]
[TestMethod]
public void TestMethod()
{
    SomeMethod();
}

Good exception types to know

  • ArgumentException
  • ArgumentNullException
  • ArgumentOutOfRangeException
  • InvalidOperationException
  • FormatException
  • DivideByZeroException

Avoid

Assert.ThrowsException<Exception>(() => SomeMethod());

Too broad unless you truly mean any exception.

Best practice

var ex = Assert.ThrowsException<ArgumentNullException>(() => service.Load(null));
Assert.AreEqual("id", ex.ParamName);

FAQ

How do I assert that an exception was thrown in MSTest?

Use Assert.ThrowsException<T>() and place the code that should fail inside the lambda.

Assert.ThrowsException<ArgumentException>(() => DoWork());

Is Assert.ThrowsException<T>() better than [ExpectedException]?

Usually yes. It is more precise, easier to read, and lets you inspect the exception object.

Can I check the exception message in MSTest?

Yes. Store the returned exception and assert its properties.

var ex = Assert.ThrowsException<ArgumentException>(() => DoWork());
Assert.AreEqual("Invalid input.", ex.Message);

What happens if no exception is thrown?

The test fails because MSTest expected an exception and did not receive one.

What happens if the wrong exception type is thrown?

The test fails. Assert.ThrowsException<T>() expects the specific exception type you provide.

Should I test exact exception messages?

Only when necessary. Messages may change. Stable properties like ParamName are often better for argument-related exceptions.

Can I still use try/catch in MSTest?

Yes, but usually only when you need more control than Assert.ThrowsException<T>() provides.

Take Quiz

Mini Project

Description

Build a small validation-focused service and test it with MSTest. The project demonstrates how to verify that methods throw the correct exceptions when invalid input is passed. This mirrors real application code where services reject bad data before doing work.

Goal

Create a ProductService and write MSTest unit tests that confirm invalid product data throws the correct exceptions.

Requirements

  • Create a ProductService class with an AddProduct(string name, decimal price) method.
  • Throw ArgumentException when the name is empty or whitespace.
  • Throw ArgumentOutOfRangeException when the price is negative.
  • Return a success message when the input is valid.
  • Write MSTest methods that verify both exception cases and one successful case.
Take Quiz

Keep learning

Related questions

View more