Walkthrough: Create and run unit tests for managed code

This article steps you through creating, running, and customizing a series of unit tests using the Microsoft unit test framework for managed code and Visual Studio Test Explorer. You start with a C# project that is under development, create tests that exercise its code, run the tests, and examine the results. Then you can change your project code and rerun the tests.

Note

This walkthrough uses the Microsoft unit test framework for managed code. Test Explorer also can run tests from third party unit test frameworks that have adapters for Test Explorer. For more information, see Install third-party unit test frameworks

For information about how to run tests from a command line, see VSTest.Console.exe command-line options.

Prerequisites

Create a project to test

  1. Open Visual Studio.

  2. On the File menu, select New > Project.

    The New Project dialog box appears.

  3. Under Installed Templates, click Visual C#.

  4. In the list of application types, click Class Library.

  5. In the Name box, type Bank and then click OK.

    The new Bank project is created and displayed in Solution Explorer with the Class1.cs file open in the code editor.

    Note

    If Class1.cs is not open in the Code Editor, double-click the file Class1.cs in Solution Explorer to open it.

  6. Copy the source code from the Sample project for creating unit tests, and replace the original contents of Class1.cs with the copied code.

  7. Save the file as BankAccount.cs.

  8. On the Build menu, click Build Solution.

You now have a project named Bank. It contains source code to test and tools to test it with. The namespace for Bank, BankAccountNS, contains the public class BankAccount, whose methods you'll test in the following procedures.

In this article, the tests focus on the Debit method. The Debit method is called when money is withdrawn from an account. Here is the method definition:

// Method to be tested.
public void Debit(double amount)
{
    if(amount > m_balance)
    {
        throw new ArgumentOutOfRangeException("amount");
    }
    if (amount < 0)
    {
        throw new ArgumentOutOfRangeException("amount");
    }
    m_balance += amount;
}

Create a unit test project

  1. On the File menu, select Add > New Project.

  2. In the New Project dialog box, expand Installed, expand Visual C#, and then choose Test.

  3. From the list of templates, select Unit Test Project.

  4. In the Name box, enter BankTests, and then select OK.

    The BankTests project is added to the Bank solution.

  5. In the BankTests project, add a reference to the Bank project.

    In Solution Explorer, select References in the BankTests project and then choose Add Reference from the context menu.

  6. In the Reference Manager dialog box, expand Solution and then check the Bank item.

Create the test class

Create a test class to verify the BankAccount class. You can use the UnitTest1.cs file that was generated by the project template, but give the file and class more descriptive names. You can do that in one step by renaming the file in Solution Explorer.

Rename a class file

In Solution Explorer, select the UnitTest1.cs file in the BankTests project. From the context menu, choose Rename, and then rename the file to BankAccountTests.cs. Choose Yes on the dialog that asks if you want to rename all references to the code element UnitTest1 in the project.

This step changes the name of the class to BankAccountTests. The BankAccountTests.cs file now contains the following code:

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace BankTests
{
    [TestClass]
    public class BankAccountTests
    {
        [TestMethod]
        public void TestMethod1()
        {
        }
    }
}

Add a using statement to the project under test

You can also add a using statement to the class to be able to call into the project under test without using fully qualified names. At the top of the class file, add:

using BankAccountNS;

Test class requirements

The minimum requirements for a test class are:

  • The [TestClass] attribute is required in the Microsoft unit testing framework for managed code for any class that contains unit test methods that you want to run in Test Explorer.

  • Each test method that you want Test Explorer to run must have the [TestMethod] attribute.

You can have other classes in a unit test project that do not have the [TestClass] attribute, and you can have other methods in test classes that do not have the [TestMethod] attribute. You can use these other classes and methods in your test methods.

Create the first test method

In this procedure, you'll write unit test methods to verify the behavior of the Debit method of the BankAccount class. The Debit method is shown previously in this article.

There are at least three behaviors that need to be checked:

  • The method throws an ArgumentOutOfRangeException if the debit amount is greater than the balance.

  • The method throws an ArgumentOutOfRangeException if the debit amount is less than zero.

  • If the debit amount is valid, the method subtracts the debit amount from the account balance.

Tip

You can delete the default TestMethod1 method, because you won't use it in this walkthrough.

To create a test method

The first test verifies that a valid amount (that is, one that is less than the account balance and greater than zero) withdraws the correct amount from the account. Add the following method to that BankAccountTests class:

[TestMethod]
public void Debit_WithValidAmount_UpdatesBalance()
{
    // Arrange
    double beginningBalance = 11.99;
    double debitAmount = 4.55;
    double expected = 7.44;
    BankAccount account = new BankAccount("Mr. Bryan Walton", beginningBalance);

    // Act
    account.Debit(debitAmount);

    // Assert
    double actual = account.Balance;
    Assert.AreEqual(expected, actual, 0.001, "Account not debited correctly");
}

The method is straightforward: it sets up a new BankAccount object with a beginning balance, and then withdraws a valid amount. It uses the AreEqual method to verify that the ending balance is as expected.

Test method requirements

A test method must meet the following requirements:

  • It's decorated with the [TestMethod] attribute.

  • It returns void.

  • It cannot have parameters.

Build and run the test

  1. On the Build menu, choose Build Solution.

    If there are no errors, Test Explorer appears with Debit_WithValidAmount_UpdatesBalance listed in the Not Run Tests group.

    Tip

    If Test Explorer does not appear after a successful build, choose Test on the menu, then choose Windows, and then choose Test Explorer.

  2. Choose Run All to run the test. While the test is running, the status bar at the top of the window is animated. At the end of the test run, the bar turns green if all the test methods pass, or red if any of the tests fail.

  3. In this case, the test fails. The test method is moved to the Failed Tests group. Select the method in Test Explorer to view the details at the bottom of the window.

Fix your code and rerun your tests

Analyze the test results

The test result contains a message that describes the failure. For the AreEqual method, the message displays what was expected (the Expected<value> parameter) and what was actually received (the Actual<value> parameter). You expected the balance to decrease, but instead it increased by the amount of the withdrawal.

The unit test has uncovered a bug: the amount of the withdrawal is added to the account balance when it should be subtracted.

Correct the bug

To correct the error, replace the line:

m_balance += amount;

with:

m_balance -= amount;

Rerun the test

In Test Explorer, choose Run All to rerun the test. The red/green bar turns green to indicate that the test passed, and the test is moved to the Passed Tests group.

Use unit tests to improve your code

This section describes how an iterative process of analysis, unit test development, and refactoring can help you make your production code more robust and effective.

Analyze the issues

You've created a test method to confirm that a valid amount is correctly deducted in the Debit method. Now, verify that the method throws an ArgumentOutOfRangeException if the debit amount is either:

  • greater than the balance, or
  • less than zero.

Create the test methods

Create a test method to verify correct behavior when the debit amount is less than zero:

[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void Debit_WhenAmountIsLessThanZero_ShouldThrowArgumentOutOfRange()
{
    // Arrange
    double beginningBalance = 11.99;
    double debitAmount = -100.00;
    BankAccount account = new BankAccount("Mr. Bryan Walton", beginningBalance);

    // Act
    account.Debit(debitAmount);

    // Assert is handled by the ExpectedException attribute on the test method.
}

Use the ExpectedExceptionAttribute attribute to assert that the correct exception has been thrown. The attribute causes the test to fail unless an ArgumentOutOfRangeException is thrown. If you temporarily modify the method under test to throw a more generic ApplicationException when the debit amount is less than zero, the test behaves correctly—that is, it fails.

To test the case when the amount withdrawn is greater than the balance, do the following steps:

  1. Create a new test method named Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange.

  2. Copy the method body from Debit_WhenAmountIsLessThanZero_ShouldThrowArgumentOutOfRange to the new method.

  3. Set the debitAmount to a number greater than the balance.

Run the tests

Running the two test methods demonstrates that the tests work correctly.

Continue the analysis

However, the last two test methods are also troubling. You can't be certain which condition in the method under test throws the exception when either test is run. Some way of differentiating the two conditions, that is a negative debit amount or an amount greater than the balance, would increase your confidence in the tests.

Look at the method under test again, and notice that both conditional statements use an ArgumentOutOfRangeException constructor that just takes name of the argument as a parameter:

throw new ArgumentOutOfRangeException("amount");

There is a constructor you can use that reports far richer information: ArgumentOutOfRangeException(String, Object, String) includes the name of the argument, the argument value, and a user-defined message. You can refactor the method under test to use this constructor. Even better, you can use publicly available type members to specify the errors.

Refactor the code under test

First, define two constants for the error messages at class scope. Put these in the class under test, BankAccount:

public const string DebitAmountExceedsBalanceMessage = "Debit amount exceeds balance";
public const string DebitAmountLessThanZeroMessage = "Debit amount is less than zero";

Then, modify the two conditional statements in the Debit method:

    if (amount > m_balance)
    {
        throw new ArgumentOutOfRangeException("amount", amount, DebitAmountExceedsBalanceMessage);
    }

    if (amount < 0)
    {
        throw new ArgumentOutOfRangeException("amount", amount, DebitAmountLessThanZeroMessage);
    }

Refactor the test methods

Remove the ExpectedException test method attribute and instead, catch the thrown exception and verify its associated message. The StringAssert.Contains method provides the ability to compare two strings.

Now, the Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange might look like this:

[TestMethod]
public void Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange()
{
    // Arrange
    double beginningBalance = 11.99;
    double debitAmount = 20.0;
    BankAccount account = new BankAccount("Mr. Bryan Walton", beginningBalance);

    // Act
    try
    {
        account.Debit(debitAmount);
    }
    catch (ArgumentOutOfRangeException e)
    {
        // Assert
        StringAssert.Contains(e.Message, BankAccount.DebitAmountExceedsBalanceMessage);
    }
}

Retest, rewrite, and reanalyze

Assume there's a bug in the method under test, and the Debit method doesn't even throw an ArgumentOutOfRangeException, nevermind output the correct message with the exception. Currently, the test method doesn't handle this case. If the debitAmount value is valid (that is, less than the balance but greater than zero), no exception is caught, so the assert never fires. Yet, the test method passes. This is not good, because you want the test method to fail if no exception is thrown.

This is a bug in the test method. To resolve the issue, add an Fail assert at the end of the test method to handle the case where no exception is thrown.

But rerunning the test shows that the test now fails if the correct exception is caught. The catch block catches the exception, but the method continues to execute and it fails at the new Fail assert. To resolve this problem, add a return statement after the StringAssert in the catch block. Rerunning the test confirms that you've fixed this problem. The final version of the Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange looks like this:

[TestMethod]
public void Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange()
{
    // Arrange
    double beginningBalance = 11.99;
    double debitAmount = 20.0;
    BankAccount account = new BankAccount("Mr. Bryan Walton", beginningBalance);

    // Act
    try
    {
        account.Debit(debitAmount);
    }
    catch (ArgumentOutOfRangeException e)
    {
        // Assert
        StringAssert.Contains(e.Message, BankAccount.DebitAmountExceedsBalanceMessage);
        return;
    }

    Assert.Fail("The expected exception was not thrown.");
}

The improvements to the test code led to more robust and informative test methods. But more importantly, they also improved the code under test.