unit testing
exception handling
assert methods
software testing
test-driven development

Best way to test exceptions with Assert to ensure they will be thrown

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

The best exception test is narrow: it executes only the code expected to fail, asserts the exact exception type, and then checks the important details of the exception. Broad try-catch tests tend to hide bugs because they can pass even when the wrong line throws or when setup code fails first.

Assert the exception type directly

Most modern test frameworks provide a built-in assertion for exception testing. Use that instead of manually catching exceptions unless the framework gives you no better option.

csharp
1using System;
2using Xunit;
3
4public class Parser
5{
6    public int ParsePort(string raw)
7    {
8        int value = int.Parse(raw);
9        if (value < 1 || value > 65535)
10        {
11            throw new ArgumentOutOfRangeException(nameof(raw), "Port must be between 1 and 65535.");
12        }
13        return value;
14    }
15}
16
17public class ParserTests
18{
19    [Fact]
20    public void ParsePort_Throws_ForZero()
21    {
22        var parser = new Parser();
23
24        Assert.Throws<ArgumentOutOfRangeException>(() => parser.ParsePort("0"));
25    }
26}

This pattern is good because the lambda contains only the operation under test. If object construction or unrelated setup fails, the test output stays clear.

Verify the exception contents

Checking the type alone is often not enough. If the code has multiple failure modes with the same exception type, assert on the message, parameter name, or custom properties.

csharp
1[Fact]
2public void ParsePort_SetsParameterName()
3{
4    var parser = new Parser();
5
6    var ex = Assert.Throws<ArgumentOutOfRangeException>(() => parser.ParsePort("70000"));
7    Assert.Equal("raw", ex.ParamName);
8}

That extra assertion proves the code failed for the expected reason instead of just failing somewhere nearby.

Keep the failing action minimal

A common anti-pattern is putting too much code inside the exception assertion. The longer the lambda or callback, the less confidence the test gives you.

Bad pattern:

csharp
1Assert.Throws<ArgumentOutOfRangeException>(() => {
2    var parser = new Parser();
3    var text = "70000";
4    parser.ParsePort(text);
5});

Better pattern:

csharp
var parser = new Parser();
Assert.Throws<ArgumentOutOfRangeException>(() => parser.ParsePort("70000"));

The setup is now outside the assertion, so the exception check covers only the intended call.

Equivalent pattern in JUnit

The same idea applies in Java. JUnit 5 provides assertThrows, which returns the exception so you can inspect it further.

java
1import static org.junit.jupiter.api.Assertions.assertEquals;
2import static org.junit.jupiter.api.Assertions.assertThrows;
3
4import org.junit.jupiter.api.Test;
5
6class PortParser {
7    int parsePort(String raw) {
8        int value = Integer.parseInt(raw);
9        if (value < 1 || value > 65535) {
10            throw new IllegalArgumentException("Port out of range");
11        }
12        return value;
13    }
14}
15
16class PortParserTest {
17    @Test
18    void parsePortThrowsForInvalidValue() {
19        PortParser parser = new PortParser();
20
21        IllegalArgumentException ex = assertThrows(
22            IllegalArgumentException.class,
23            () -> parser.parsePort("70000")
24        );
25
26        assertEquals("Port out of range", ex.getMessage());
27    }
28}

The framework-specific syntax changes, but the testing principle is the same.

Test asynchronous exceptions differently when needed

If the code returns a Task, Future, or promise-like object, use the async-specific assertion helper. A synchronous exception assertion will often inspect the wrong thing.

For example, in xUnit you would use Assert.ThrowsAsync<TException>. In JUnit, you may need to block on the future inside the assertion and assert the wrapped exception shape carefully.

The key point is that the assertion must match the execution model of the code under test.

Common Pitfalls

  • Catching Exception manually and treating any failure as a passing test.
  • Putting setup code inside the exception assertion and letting the wrong line satisfy the test.
  • Verifying only the exception type when the message or parameter name is part of the contract.
  • Using synchronous exception assertions for asynchronous APIs.
  • Writing tests that pass on any subclass when the code should throw one precise exception type.

Summary

  • Use framework-provided exception assertions such as Assert.Throws or assertThrows.
  • Keep only the failing operation inside the asserted callback.
  • Inspect the exception object when its details matter.
  • Match the assertion style to the code's execution model, especially for async code.
  • Prefer precise exception assertions over broad try-catch logic.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Basis Lab
No course covers this one yet

Basis Lab writes one for you from a sentence about what you want to be able to do, then teaches it and checks you understood. Your first course is free.

Build my course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions