Writing Tests

Name each test for its scenario, write it in three parts (Arrange, Act, Assert), check results with MSTest's Assert methods, and run one test for many inputs with DataRow.

Apprentice · Unit Testing · C# · Updated

After this lesson you can

  • Write a test in three parts, Arrange, Act and Assert, named for its scenario
  • Check results with Assert methods such as AreEqual, IsTrue, IsFalse and IsNull
  • Run one test for several inputs with [DataRow]

Key takeaways

  • Name each test for its scenario, as MethodName_WhenThisIsTrue_ExpectedResult, such as ChangeChannel_WhenOff_KeepsTheChannel.
  • Arrange sets up the starting state, Act calls the one method being tested, and Assert checks both the return value and the object's properties.
  • Assert has a method for each kind of check, such as AreEqual, IsTrue, IsFalse and IsNull. When a check fails, the test fails.
  • In Assert.AreEqual, put the expected value first and the actual value second, and add a message that explains the requirement.
  • [DataRow] runs one test once for each input, and each run counts as a test of its own.

Each row of your scenario table becomes one test method. Here's how to write them so they're easy to read, and so a failure tells you exactly what broke.

Name the scenario

A test's name should read like the scenario. This course names tests in three parts, separated by underscores:

MethodName_WhenThisIsTrue_ExpectedResult
ChannelUp_WhenOn_GoesUpOneChannel
ChannelUp_AtTheHighestChannel_GoesBackToTheLowest
ChangeChannel_WhenOff_KeepsTheChannel

Long names are fine. When one of a hundred tests fails, its name is the first thing you see, and ChangeChannel_WhenOff_KeepsTheChannel tells you far more than Test7.

Arrange, Act, Assert

Every test has three parts, in this order:

Part What it does
Arrange Sets up the scenario's starting state
Act Calls the one method being tested
Assert Checks the result: the return value, and the object's properties
[TestMethod]
public void ChannelUp_WhenOn_GoesUpOneChannel()
{
    // Arrange
    Television tv = new Television();
    tv.TurnOn();

    // Act
    int newChannel = tv.ChannelUp();

    // Assert
    Assert.AreEqual(4, newChannel, "ChannelUp returns the new channel.");
    Assert.AreEqual(4, tv.CurrentChannel, "The TV is now on the new channel.");
}

Notice the Assert part checks both the returned value and the property. A method that returns 4 but leaves the TV on channel 3 would pass the first check and fail the second.

Another scenario, starting from a different state:

[TestMethod]
public void ChannelUp_AtTheHighestChannel_GoesBackToTheLowest()
{
    // Arrange
    Television tv = new Television();
    tv.TurnOn();
    tv.ChangeChannel(15);

    // Act
    int newChannel = tv.ChannelUp();

    // Assert
    Assert.AreEqual(3, newChannel);
}

The Assert class

MSTest's Assert class has a method for each kind of check. If the check fails, the test fails, and the runner shows why.

Method Passes when
Assert.AreEqual(expected, actual) The two values are equal
Assert.AreNotEqual(notExpected, actual) The two values are different
Assert.IsTrue(condition) The condition is true
Assert.IsFalse(condition) The condition is false
Assert.IsNull(value) The value is null
Assert.IsNotNull(value) The value isn't null
Assert.Contains(part, text) The text contains the part

Two habits make failures easy to read:

  • Expected first, actual second, in AreEqual. The failure message labels them that way.
  • Add a message that explains the requirement. It's printed when the test fails.

Here's a test that expects the wrong thing, and what the runner says:

Assert.AreEqual(5, tv.CurrentChannel, "Because the TV started on channel 3.");
Failed ChannelUp_WhenOn_GoesUpOneChannel
  Error Message:
   Assert.AreEqual failed. Expected:<5>. Actual:<4>. 'expected' expression: '5', 'actual' expression: 'tv.CurrentChannel'. Because the TV started on channel 3.

Many inputs, one test: DataRow

Some scenarios are the same test with different inputs. Instead of copying the test, give it parameters and one [DataRow] per input:

[TestMethod]
[DataRow(3)]
[DataRow(7)]
[DataRow(15)]
public void ChangeChannel_ToAChannelThatExists_ChangesIt(int channel)
{
    // Arrange
    Television tv = new Television();
    tv.TurnOn();

    // Act
    bool changed = tv.ChangeChannel(channel);

    // Assert
    Assert.IsTrue(changed);
    Assert.AreEqual(channel, tv.CurrentChannel);
}

The runner runs it once per DataRow, and counts each run as a test of its own, so this method counts as three tests. A DataRow can hold several values, one per parameter: [DataRow("banana", 3)] for (string text, int expected).

Check yourself

3 questions to try on the lesson page.

So what

A good test reads like a sentence: its name says the scenario, Arrange sets it up, Act does one thing, and Assert checks what the requirement promised. Write them that way, and a failing test tells you what's wrong before you open the code.

Key terms

Cheat sheets