Question
How to Assert Exceptions in MSTest (C#)
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.
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)throwsDivideByZeroException, 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:
- The test starts running.
Assert.ThrowsException<ArgumentNullException>prepares to execute the lambda.- The lambda calls
SaveFile(null). - Inside
SaveFile, the conditionpath == nullis true. - The method throws
ArgumentNullException. - MSTest catches that exception.
- MSTest checks whether the caught exception is exactly the expected type.
- Because it matches, the assertion passes.
- 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 calledage < 0is 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
ArgumentExceptionfor invalid email input
- A registration method throws
- Null checks
- A service throws
ArgumentNullExceptionwhen a required dependency or parameter is missing
- A service throws
- Business rules
- An order service throws
InvalidOperationExceptionif checkout is attempted with an empty cart
- An order service throws
- File and I/O handling
- A file loader throws
FileNotFoundExceptionfor missing files
- A file loader throws
- Parsing and conversion
- A parser throws
FormatExceptionwhen input is not in the correct format
- A parser throws
- Security and authorization
- A method throws
UnauthorizedAccessExceptionwhen a user lacks permission
- A method throws
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);
});
}
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
| Approach | Example | Pros | Cons | Recommended? |
|---|---|---|---|---|
Assert.ThrowsException<T>() | Assert.ThrowsException<ArgumentException>(() => DoWork()); | Clear, precise, can inspect returned exception | Slightly more verbose than an attribute | Yes |
[ExpectedException] | [ExpectedException(typeof(ArgumentException))] | Simple for very small tests | Less precise, exception can come from any line | Usually no |
Manual try/catch | try { ... } catch (...) { ... } | Full control | More boilerplate, easier to get wrong | Only when needed |
Assert.ThrowsException<T>() vs [ExpectedException]
| Feature | Assert.ThrowsException<T>() | [ExpectedException] |
|---|---|---|
| Exception location is clear | Yes | No |
| Can inspect exception object | Yes | No |
| Keeps setup outside assertion | Yes | Less naturally |
| Good for modern MSTest code | Yes | Mostly 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);
}
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
ArgumentExceptionArgumentNullExceptionArgumentOutOfRangeExceptionInvalidOperationExceptionFormatExceptionDivideByZeroException
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.
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
ProductServiceclass with anAddProduct(string name, decimal price)method. - Throw
ArgumentExceptionwhen the name is empty or whitespace. - Throw
ArgumentOutOfRangeExceptionwhen 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.
Keep learning
Related questions
AddTransient vs AddScoped vs AddSingleton in ASP.NET Core Dependency Injection
Learn the differences between AddTransient, AddScoped, and AddSingleton in ASP.NET Core DI with examples and practical usage.
Best Way to Repeat a Character in C#: Building Repeated Strings Efficiently
Learn the best way to repeat a character in C#, compare StringBuilder, string concatenation, and simpler built-in options.
C# Access Modifiers and static: public, private, protected, and Defaults
Learn how C# public, private, protected, and default access control visibility, and how static differs from instance members.