C# · Course navigation

C#: testing values and exceptions with a small harness

Check results, and verify that bad input throws the expected exception type.

On this page
Before you start

C# exceptions, generics, and delegates.

  • Check results, and verify that bad input throws the expected exception type.
  • Predict the sample output and explain the state changes.
  • Complete the practice task and check the boundary cases.

The concept

A unit test calls a small piece of code with chosen input and checks the output or the error. Frameworks such as xUnit, NUnit, and MSTest do this at scale. The core is small: a function that compares an expected value with an actual one and reports the result.

Testing error handling needs a way to say that code must throw. The AssertThrows method takes the code as an Action delegate and catches the exception type given as a generic argument. If the code throws nothing, or a different exception, the test fails. Good tests cover normal values, boundaries, and error cases.

Code example

C#
using System;

class Program {
  static int failures = 0;

  static int Percent(string text) {
    int value = int.Parse(text);
    if (value < 0 || value > 100) {
      throw new ArgumentOutOfRangeException("text", "percent out of range");
    }
    return value;
  }

  static void AssertEqual(string name, int expected, int actual) {
    if (expected == actual) {
      Console.WriteLine("PASS " + name);
    } else {
      failures++;
      Console.WriteLine("FAIL " + name + ": expected " + expected + " but was " + actual);
    }
  }

  static void AssertThrows<T>(string name, Action code) where T : Exception {
    try {
      code();
      failures++;
      Console.WriteLine("FAIL " + name + ": nothing was thrown");
    } catch (T) {
      Console.WriteLine("PASS " + name);
    }
  }

  static void Main() {
    AssertEqual("parses 42", 42, Percent("42"));
    AssertEqual("parses 0", 0, Percent("0"));
    AssertThrows<ArgumentOutOfRangeException>("rejects 101", () => Percent("101"));
    AssertThrows<FormatException>("rejects text", () => Percent("abc"));
    Console.WriteLine("failures: " + failures);
  }
}
Output
PASS parses 42
PASS parses 0
PASS rejects 101
PASS rejects text
failures: 0

Follow the code

Open the walkthrough to inspect the example one step at a time.

Show guided walkthrough
Guided walkthroughReady
1using System;
2
3class Program {
4 static int failures = 0;
5
6 static int Percent(string text) {
7 int value = int.Parse(text);
8 if (value < 0 || value > 100) {
9 throw new ArgumentOutOfRangeException("text", "percent out of range");
10 }
11 return value;
12 }
13
14 static void AssertEqual(string name, int expected, int actual) {
15 if (expected == actual) {
16 Console.WriteLine("PASS " + name);
17 } else {
18 failures++;
19 Console.WriteLine("FAIL " + name + ": expected " + expected + " but was " + actual);
20 }
21 }
22
23 static void AssertThrows<T>(string name, Action code) where T : Exception {
24 try {
25 code();
26 failures++;
27 Console.WriteLine("FAIL " + name + ": nothing was thrown");
28 } catch (T) {
29 Console.WriteLine("PASS " + name);
30 }
31 }
32
33 static void Main() {
34 AssertEqual("parses 42", 42, Percent("42"));
35 AssertEqual("parses 0", 0, Percent("0"));
36 AssertThrows<ArgumentOutOfRangeException>("rejects 101", () => Percent("101"));
37 AssertThrows<FormatException>("rejects text", () => Percent("abc"));
38 Console.WriteLine("failures: " + failures);
39 }
40}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
A generic helper: T is the exception type that must be thrown.—
—
A normal case.—
PASS parses 42
101 is out of range, so the expected exception is thrown and caught.—
PASS parses 42
PASS parses 0
PASS rejects 101
int.Parse rejects text with a FormatException.—
PASS parses 42
PASS parses 0
PASS rejects 101
PASS rejects text
No check failed.failures = 0
PASS parses 42
PASS parses 0
PASS rejects 101
PASS rejects text
failures: 0

Behind the scenes

How C# processes this code

C# compiles to code for the .NET runtime, which invokes the program entry point.

Cost and common mistakes

Each check is O(1) plus the cost of the code under test.

  • A catch for the wrong exception type lets a different error escape, which also fails the test, as it should.
  • A test that cannot fail is worthless. Break the code on purpose once to confirm that the test notices.
  • int.TryParse avoids exceptions for ordinary bad input in production code; exceptions are for exceptional cases.

Try it yourself

Add a boundary test for 100.

Reveal answer and reasoning

AssertEqual("parses 100", 100, Percent("100")); 100 is the largest accepted value.

Further reading