ScreenshotNeo

BlogHow-to

How to Write Exception Tests in TestNG

Use TestNG’s expectedExceptions for a method-wide check, or Assert.expectThrows to check one call and inspect the exception. Includes runnable Java examples and fixes for common mistakes.

By the ScreenshotNeo team4 October 20266 min read

To make a TestNG test pass only when a method throws a particular exception, add expectedExceptions to @Test:

@Test(expectedExceptions = IllegalArgumentException.class)
public void rejectsInvalidInput() {
    service.process(null);
}

The test passes when the method throws the expected exception type. It fails if no exception is thrown or if a different exception escapes. Use this annotation when the exception from the entire test method is the behavior you want to assert. For a single call, or when you need to inspect the exception, use Assert.expectThrows.

1. Set up a minimal TestNG test

The examples use Java and TestNG. Add TestNG to the test dependencies using your build tool. For Maven, a typical test-scoped dependency is:

<dependency>
  <groupId>org.testng</groupId>
  <artifactId>testng</artifactId>
  <version>7.11.0</version>
  <scope>test</scope>
</dependency>

For Gradle, the equivalent dependency notation is:

dependencies {
    testImplementation 'org.testng:testng:7.11.0'
}

Use the version already managed by your project if it differs. The annotation example below is documented in TestNG 7.11.0. The Assert.expectThrows API is documented in 7.9.0 and is marked as available since TestNG 6.9.5; confirm that your project’s actual dependency exposes it.

2. Expect an exception from the test method

Put the expected exception class on the test annotation. Keep the test focused so only the operation under test can satisfy the expectation.

import org.testng.annotations.Test;

public class InputValidationTest {
    private final InputService service = new InputService();

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void rejectsNullInput() {
        service.process(null);
    }
}

The exception must escape the test method. If the method returns normally, TestNG marks it failed because the expected exception was not observed. If another exception type escapes, the test also fails.

You can list multiple acceptable exception classes when the contract intentionally allows more than one:

@Test(expectedExceptions = {
    IllegalArgumentException.class,
    NullPointerException.class
})
public void rejectsInvalidInput() {
    service.process(null);
}

Prefer the narrowest type the method contract promises. A broad superclass can allow an unintended failure mode to pass. The annotation applies to the test method as a whole, so avoid setup or unrelated calls that could throw one of the accepted types.

3. Check the exception message

Use expectedExceptionsMessageRegExp alongside expectedExceptions when the message is part of the behavior being tested.

@Test(
    expectedExceptions = IllegalArgumentException.class,
    expectedExceptionsMessageRegExp = ".*must not be null.*"
)
public void rejectsNullInputWithUsefulMessage() {
    service.process(null);
}

The message option is a regular-expression match, not a plain substring comparison. Its default is .*, which does not constrain the message. Escape regex metacharacters when asserting literal punctuation, and avoid matching values that vary across environments unless that variation is itself important.

4. Scope the assertion to one call with expectThrows

Use Assert.expectThrows when setup or other assertions should not be allowed to satisfy the exception expectation, or when you need the exception object for more checks.

import org.testng.Assert;
import org.testng.annotations.Test;

public class InputValidationTest {
    private final InputService service = new InputService();

    @Test
    public void rejectsNullInputWithUsefulMessage() {
        IllegalArgumentException exception = Assert.expectThrows(
            IllegalArgumentException.class,
            () -> service.process(null)
        );

        Assert.assertTrue(exception.getMessage().contains("must not be null"));
    }
}

This form runs the supplied action, returns the expected exception for further assertions, and fails if the action does not throw or throws the wrong type. It scopes the check to the lambda, so code before and after it can run normally.

Use expectedExceptions for a compact whole-method expectation. Use expectThrows when the boundary of the expected failure matters or the exception’s fields and message need inspection.

5. Use try/catch when needed

If your TestNG version or project conventions do not support expectThrows, a try/catch with an explicit failure assertion is a straightforward scoped alternative:

import org.testng.Assert;
import org.testng.annotations.Test;

@Test
public void rejectsNullInputWithUsefulMessage() {
    try {
        service.process(null);
        Assert.fail("Expected IllegalArgumentException");
    } catch (IllegalArgumentException exception) {
        Assert.assertTrue(exception.getMessage().contains("must not be null"));
    }
}

Because Assert.fail raises an assertion failure rather than IllegalArgumentException, a missing exception cannot accidentally look like success.

6. Choose the right assertion shape

Need Use Scope
The test method itself should throw one type @Test(expectedExceptions = Type.class) Whole test method
The exception message must match a pattern expectedExceptionsMessageRegExp Whole test method
Only one operation should throw Assert.expectThrows Runnable passed to the assertion
Need assertions on the exception object Assert.expectThrows Runnable, then inspect returned exception
Older API compatibility or custom catch logic Try/catch plus Assert.fail Explicitly scoped call

7. Common mistakes and fixes

Symptom Cause Fix
The test fails because no exception was thrown The input did not trigger the failure, or the code caught the exception internally. Check the test input and method contract. With expectedExceptions, let the expected exception escape the method. Use expectThrows if the call needs a scoped assertion.
The test fails with an unexpected exception The implementation threw a different type, or setup failed first. Read the reported exception and stack trace. Keep setup outside the expected operation where possible, then assert the specific documented type.
The annotation test passes because an unrelated line throws The annotation covers the entire method, not just the intended invocation. Reduce the method to the operation under test or replace the annotation with expectThrows around that call.
The exception is caught but TestNG reports the expected exception was missing The test caught it, so it did not escape the method. Use expectThrows or try/catch assertions for an exception that should be handled inside the test.
The message assertion is too permissive or fails on punctuation The annotation uses a regular expression, and regex characters have special meanings. Use a constraining pattern; escape literal regex metacharacters. For substring checks, use expectThrows and inspect the message directly.
A test unexpectedly passes for a subclass exception The expected type is broad enough to include that subtype. Choose the narrowest expected class that expresses the contract.
expectThrows cannot be resolved The project’s TestNG version or imports do not provide the API. Check the resolved TestNG dependency and import org.testng.Assert. Upgrade only if compatible with the project, or use the try/catch pattern.

8. Reliability and maintenance

  • Make each exception test name state the invalid condition and the promised failure, such as rejectsNullInput.
  • Assert only stable parts of exception messages. Messages can change as implementation details change; test them when they are part of the public behavior.
  • Keep validation tests deterministic. Avoid relying on environment-specific state when a direct invalid input can exercise the contract.
  • Do not use an expected exception as a substitute for checking a meaningful result when the method should succeed.
  • When multiple failure modes are valid, list them intentionally and document why each is part of the contract.

9. Or skip the browser setup

Exception tests are usually local Java tests, so a screenshot API is not part of the assertion. If your developer workflow also needs repeatable website captures for visual checks or documentation, ScreenshotNeo provides a website screenshot API and MCP server. A single GET request returns a PNG, JPEG, WebP, or PDF; its options include full-page and element captures, device presets, custom headers, waits, and more. See the API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', new Uint8Array(await res.arrayBuffer()));

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, with response headers indicating the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 shots.

Sign up free for 1,000 screenshots a month, no card required.

10. FAQ

Can I check that an exception is thrown without checking its message?

Yes. Specify only expectedExceptions, or use expectThrows and ignore the returned exception if no further inspection is needed.

Does expectedExceptions check only the line that calls my method?

No. It applies to exceptions escaping the entire annotated test method. Use expectThrows to target one call.

Should every exception test assert a message?

No. Assert message content when it is part of the behavior your code promises. Otherwise, checking the exception type is usually less brittle.

Sources