Programming with the Unit Test Framework


In this section, we describe in detail the attributes and methods available to you for creating unit tests. All of the classes and attributes mentioned in this section can be found in the Microsoft.VisualStudio.TestTools.UnitTesting namespace.

Initialization and cleanup of unit tests

Often, you'll need to configure a resource that is shared among your tests. Examples might be a database connection, a log file, or a shared object in a known default state. You might also need ways to clean up from the actions of your tests, such as closing a shared stream or rolling back a transaction.

The unit test framework offers attributes to identify such methods. They are grouped into three levels: Test, Class, and Assembly. The levels determine the scope and timing of execution for the methods they decorate. The following table describes these attributes.

Attributes

Frequency and Scope

TestInitialize, TestCleanup

Executed before (Initialize) or after (Cleanup) any of the class's unit tests are run

ClassInitialize, ClassCleanup

Executed a single time before or after any of the tests in the current class are run

AssemblyInitialize, AssemblyCleanup

Executed a single time before or after any number of tests in any of the assembly's classes are run

Having methods with these attributes is optional, but do not define more than one of each attribute in the same context.

Important

Do not use class- or assembly-level initialize and cleanup attributes with ASP.NET unit tests. When run under ASP.NET, these methods cannot be guaranteed to run only once. Because these are static methods, this may lead to false testing results.

TestInitialize and TestCleanup attributes

Use the TestInitialize attribute to create a method that will be executed one time before each unit test run in the current class. Similarly, TestCleanup marks a method that will always run immediately after each test. Like unit tests, methods with these attributes must be public, nonstatic, accept no parameters, and have no return values.

Here is an example test for a simplistic shopping cart class. It contains two tests and defines the TestInitialize and TestCleanup methods:

     using Microsoft.VisualStudio.TestTools.UnitTesting;     [TestClass]     public class ShoppingCartTest     {         private ShoppingCart cart;         [TestInitialize]         public void TestInitialize()         {             cart = new SomeClass();             cart.Add(new Item("Test");)         }         [TestCleanup]         public void TestCleanup()         {              // Not required - here for illustration              cart.Dispose();         }         [TestMethod]         public void TestCountAfterAdd()         {             int expected = cart.Count + 1;             cart.Add(new Item("New Item");)             Assert.AreEqual(expected, cart.Count);         }         [TestMethod]         public void TestCountAfterRemove()         {             int expected = cart.Count - 1;             cart.Remove(0);             Assert.AreEqual(expected, cart.Count);         }     } 

When you run both tests, TestInitialize and TestCleanup are both executed twice. TestInitialize is run immediately before each unit test and TestCleanup immediately after.

ClassInitialize and ClassCleanup attributes

The ClassInitialize and ClassCleanup attributes are used very similarly to TestInitialize and TestCleanup. The difference is that these methods are guaranteed to run once and only once no matter how many unit tests are executed from the current class. Unlike TestInitialize and TestCleanup, these methods are marked static and accept a TestContext instance as a parameter.

The importance of the TestContext instance is described later in this chapter.

The following code demonstrates how you might manage a shared logging target using class-level initialization and cleanup with a logging file:

     private System.IO.File logFile;     [ClassInitialize]     public static void ClassInitialize(TestContext context)     {        // Code to open the logFile object     }     [ClassCleanup]     public static void ClassCleanup(TestContext context)     {        // Code to close the logFile object     } 

You could now reference the logFile object from any of your unit tests in this class, knowing that it will automatically be opened before any unit test is executed and closed after the final test in the class has completed.

Note

This approach to logging is simply for illustration. You'll see later how the TestContext object passed into these methods enables you to more effectively log details from your unit tests.

The following code shows the flow of execution should you again run both tests:

     ClassInitialize        TestInitialize           TestCountAfterAdd        TestCleanup        TestInitialize           TestCountAfterRemove        TestCleanup     ClassCleanup 

AssemblyInitialize and AssemblyCleanup attributes

Where you might use ClassInitialize and ClassCleanup to control operations at a class level, use the AssemblyInitialize and AssemblyCleanup attributes for an entire assembly. For example, a method decorated with AssemblyInitialize will be executed once before any test in that current assembly, not just those in the current class. As with the class-level initialize and cleanup methods, these must be static and accept a TestContext parameter:

     [AssemblyInitialize]     public static void AssemblyInitialize(TestContext context)     {        // Assembly-wide initialization code     }     [AssemblyCleanup]     public static void AssemblyCleanup(TestContext context)     {        // Assembly-wide cleanup code     } 

Consider using AssemblyInitialize and AssemblyCleanup in cases where you have common operations spanning multiple classes. Instead of having many per-class initialize and cleanup methods, you can refactor these to single assembly-level methods.

Using the Assert methods

The most common way to determine success in unit tests is to compare an expected result against an actual result. The Assert class features many methods that enable you to make these comparisons quickly.

Assert.AreEqual and Assert.AreNotEqual

Of the various Assert methods, you will likely find the most use for AreEqual and AreNotEqual. As their names imply, you are comparing an expected value to a supplied value. If the operands are not value-equivalent (or are equivalent for AreNotEqual), then the current test will fail.

A third, optional argument can be supplied: a string that will be displayed along with your unit test results, which you can use to describe the failure. Additionally, you can supply parameters to be replaced in the string, just as the String.Format method supports:

     [TestMethod]     public void IsPrimeTest()     {        const int FACTOR = 5;        const bool EXPECTED = true;        bool actual = CustomMath.IsPrime(FACTOR);        Assert.AreEqual(EXPECTED, actual, "The number {0} should have been computed as prime, but was not.", FACTOR);     }

Assert.AreEqual and AreNotEqual have many parameter overloads, accepting types such as string, double, int, float, object, and generic types. Take the time to review the overloads in the Object Browser.

When using these methods with two string arguments, one of the overrides allows you to optionally supply a third argument. This is a Boolean, called ignoreCase, that indicates whether the comparison should be case-insensitive. The default comparison is case-sensitive.

Working with floating-point numbers involves a degree of imprecision. You can supply an argument that defines a delta by which two numbers can differ yet still pass a test — for example, if you're computing square roots and decide that a "drift" of plus or minus 0.0001 is acceptable:

     [TestMethod]     public void SquareRootTeset()     {         const double EXPECTED = 3.1622;         const double DELTA = 0.0001;         double actual = CustomMath.SquareRoot(10);        Assert.AreEqual(EXPECTED, actual, DELTA, "Root not within acceptable range");     } 

Assert.AreSame and Assert.AreNotSame

AreSame and AreNotSame function in much the same manner as AreEqual and AreNotEqual. The important difference is that these methods compare the references of the supplied arguments. For example, if two arguments point to the same object instance, then AreSame will pass. Even when the arguments are exactly equivalent in terms of their state, AreSame will fail if they are not in fact the same object. This is the same concept that differentiates object.Equals from object.ReferenceEquals.

A common use for these methods is to ensure that properties return expected instances or that collections handle references correctly. In the following example, we add an item to a collection and ensure that what we get back from the collection's indexer is a reference to the same item instance:

     [TestMethod]     public void CollectionTest()     {         CustomCollection cc = new CustomCollection();         Item original = new Item("Expected");         cc.Add(original);         Item actual = cc[0];         Assert.AreSame(original, actual);     } 

Assert.IsTrue and Assert.IsFalse

As you can probably guess, IsTrue and IsFalse are used simply to ensure that the supplied expression is true or false as expected. Returning to the IsPrimeNumberTest example, we can restate it as follows:

     [TestMethod]     public void IsPrimeTest()     {        const int FACTOR = 5;        Assert.IsTrue(CustomMath.IsPrime(FACTOR), "The number {0} should have been computed as prime, but was not.", FACTOR);     } 

Assert.IsNull and Assert.IsNotNull

Similar to IsTrue and IsFalse, these methods verify that a given object type is either null or not null. Revising the collection example, this ensures that the item returned by the indexer is not null:

     [TestMethod]     public void CollectionTest()     {         CustomCollection cc = new CustomCollection();         cc.Add(new Item("Added"));         Item item = cc[0];         Assert.IsNotNull(item);     }

Assert.IsInstanceOfType and Assert.IsNotInstanceOfType

IsInstanceOfType simply ensures that a given object is an instance of an expected type. For example, suppose you have a collection that accepts entries of any type. You'd like to ensure that an entry you're retrieving is of the expected type:

     [TestMethod]     public void CollectionTest()     {         UntypedCollection untyped = new UntypedCollection();         untyped.Add(new Item("Added"));         untyped.Add(new Person("Rachel"));         untyped.Add(new Item("Another"));         object entry = untyped[1];         Assert.IsInstanceOfType(entry, typeof(Person));     } 

As you can no doubt guess, IsNotInstanceOfType will test to ensure that an object is not the specified type.

Assert.Fail and Assert.Inconclusive

Use Assert.Fail to immediately fail a test. For example, you may have a conditional case that should never occur. If it does, call Assert.Fail and an AssertFailedException will be thrown, causing the test to abort with failure. You may find Assert.Fail useful when defining your own custom Assert methods.

Assert.Inconclusive enables you to indicate that the test result cannot be verified as a pass or fail. This is typically a temporary measure until a unit test (or the related implementation) has been completed. As described in the section "Code Generation" later in this chapter, Assert.Inconclusive is used to indicate that more work is needed to be done to complete a unit test.

Note

There is no Assert.Succeed because success is indicated by completion of a unit test method without a thrown exception. Use a return statement if you wish to cause this result from some point in your test.

Assert.Fail and Assert.Inconclusive both support a string argument and optional arguments, which will be inserted into the string in the same manner as String.Format. Use this string to supply a detailed message back to the Test Results window, describing the reasons for the nonpassing result.

Using the CollectionAssert class

The Microsoft.VisualStudio.TestTools.UnitTesting namespace includes a class, Collection Assert, containing useful methods for testing the contents and behavior of collection types.

The following table describes the methods supported by CollectionAssert.

Method

Description

AllItemsAreInstancesOfType

Ensures that all elements are of an expected type

AllItemsAreNotNull

Ensures that no items in the collection are null

AllItemsAreUnique

Searches a collection, failing if a duplicate member is found

AreEqual

Ensures that two collections have reference-equivalent members

AreNotEqual

Ensures that two collections do not have reference-equivalent members

AreEquivalent

Ensures that two collections have value-equivalent members

AreNotEquivalent

Ensures that two collections do not have value-equivalent members

Contains

Searches a collection, failing if the given object is not found

DoesNotContain

Searches a collection, failing if a given object is found

IsNotSubsetOf

Ensures that the first collection has members not found in the second

IsSubsetOf

Ensures that all elements in the first collection are found in the second

The following example uses some of these methods to verify various behaviors of a collection type, CustomCollection. When this example is run, none of the assertions fail and the test results in success. Note that proper unit testing would spread these checks across multiple smaller tests.

     [TestMethod]     public void CollectionTests()     {         CustomCollection list1 = new CustomCollection();         list1.Add("alpha");         list1.Add("beta");         list1.Add("delta");         list1.Add("delta");         CollectionAssert.AllItemsAreInstancesOfType(list1, typeof(string));         CollectionAssert.AllItemsAreNotNull(list1);         CustomCollection list2 = (CustomCollection)list1.Clone();         CollectionAssert.AreEqual(list1, list2);         CollectionAssert.AreEquivalent(list1, list2);         CustomCollection list3 = new CustomCollection();         list3.Add("beta");         list3.Add("delta");         CollectionAssert.AreNotEquivalent(list3, list1);         CollectionAssert.IsSubsetOf(list3, list1);         CollectionAssert.DoesNotContain(list3, "alpha");         CollectionAssert.AllItemsAreUnique(list3);     } 

The final assertion, AllItemsAreUnique(list3), would have failed if tested against list1 because that collection has two entries of the string "delta."

Using the StringAssert class

Similar to CollectionAssert, the StringAssert class contains methods that enable you to easily make assertions based on common text operations. The following table describes the methods supported by StringAssert.

Method

Description

Contains

Searches a string for a substring and fails if not found

DoesNotMatch

Applies a regular expression to a string and fails if any matches are found

EndsWith

Fails if the string does not end with a given substring

Matches

Applies a regular expression to a string and fails if no matches are found

StartsWith

Fails if the string does not begin with a given substring

Here are some simple examples of these methods. Each of these assertions will pass:

     [TestMethod]     public void TextTests()     {         StringAssert.Contains("This is the searched text", "searched");         StringAssert.EndsWith("String which ends with searched", "ends with searched");         StringAssert.Matches("Search this string for whitespace",                              new System.Text.RegularExpressions.Regex(@"\s+"));         StringAssert.DoesNotMatch("Doesnotcontainwhitespace",                                   new System.Text.RegularExpressions.Regex(@"\s+"));         StringAssert.StartsWith("Starts with correct text", "Starts with");     } 

Matches and DoesNotMatch accept a string and an instance of System.Text.RegularExpressions.Regex. In the preceding example, a simple regular expression that looks for at least one whitespace character was used. Matches finds whitespace and the DoesNotMatch does not find whitespace, so both pass.

Expecting exceptions

Normally, a unit test that throws an exception is considered to have failed. However, you'll often wish to verify that a class behaves correctly by throwing an exception. For example, you might provide invalid arguments to a method to verify that it properly throws an exception.

The ExpectedException attribute indicates that a test will succeed only if the indicated exception is thrown. Not throwing an exception or throwing an exception of a different type will result in test failure.

The following unit test expects that an ObjectDisposedException will be thrown:

     [TestMethod]     [ExpectedException(typeof(ObjectDisposedException))]     public void ReadAfterDispose()     {         CustomFileReader cfr = new CustomFileReader("target.txt");         cfr.Dispose();         string contents = cfr.Read(); // Should throw ObjectDisposedException     } 

The ExpectedException attribute supports a second, optional string argument. The Message property of the thrown exception must match this string or the test will fail. This enables you to differentiate between two different instances of the same exception type.

For example, suppose you are calling a method that throws a FileNotFoundException for several different files. To ensure that it cannot find one specific file in your testing scenario, supply the message you expect as the second argument to ExpectedException. If the exception thrown is not FileNotFoundException and its Message property does not match that text, the test will fail.

Defining custom unit test properties

You may define custom properties for your unit tests. For example, you may wish to specify the author of each test and be able to view that property from the Test Manager.

Use the TestProperty attribute to decorate a unit test, supplying the name of the property and a value:

     [TestMethod]     [TestProperty("Author", "Deborah")]     public void ExampleTest()     {        // Test logic     } 

Now, when you view the properties of that test, you will see a new entry, Author, with the value Deborah. If you change that value from the Properties window, the attribute in your code will automatically be updated.

TestContext class

Unit tests normally have a reference to a TestContext instance. This object provides run-time features that might be useful to tests, such as details of the test itself, the various directories in use, and several methods to supplement the details stored with the test's results. TestContext is also very important for data-driven and ASP.NET unit tests, as you will see later.

Several methods are especially useful to all unit tests. The first, WriteLine, enables you to insert text into the results of your unit test. This can be useful for supplying additional information about the test, such as parameters, environment details, and other debugging data that would normally be excluded from test results. By default, information from the test run is stored in a test results file, an XML file with a .trx extension. These files can be found in the TestResults subdirectory of your project. The default name for the files is based on the user, machine, and date of the test run, but this can be modified via the test run configuration settings. See Chapter 13 for more information on test results files.

Here is a simple example of a unit test that accesses the TestContext to send a string containing the test's name to the results:

     [TestClass]     public class TestClass     {        private TestContext testContextInstance;        public TestContext TestContext        {           get { return testContextInstance; }           set { testContextInstance = value; }        }        [TestMethod]        public void TestMethod1()        {           TestContext.WriteLine("This is test {0}", TestContext.TestName);        } 

The AddResultFile method enables you to add a file, at runtime, to the results of the test run. The file you specify will be copied to the results directory alongside other results content. For example, this may be useful if your unit test is validating an object that creates or alters a file and you would like that file to be included with the results for analysis.

Finally, the BeginTimer and EndTimer methods enable you to create one or more named timers within your unit tests. The results of these timers are stored in the test run's results.

Creating data-driven unit tests

An excellent way to verify the correct behavior of code is to execute it using realistic data. Team System provides features to automatically bind data from a data source as input to unit tests. The unit test is run once for each data row.

A unit test is made data-driven by assigning attributes for connecting to and reading from a data source. The easiest way to do this is to modify an existing unit test's properties in the Test Manager window. Begin with a normal unit test outline, and then open Test Manager. Select the unit test and view its properties.

First, establish the connection to the data source by setting the Data Connection String property. You may either enter it manually or click the button labeled with ellipses ("…") to use dialogs to create the connection string. Once the connection is specified, select the table you wish to use in the Data Table Name property.

As mentioned before, the unit test will be called once per row in the data source. You can set how rows are fed into the unit test via the Data Access Method property. Sequential will feed the rows in exactly the order returned from the data source, whereas Random will select random rows. Rows are provided to the unit test until all rows have been used once.

Setting these properties will automatically decorate the selected unit test with the appropriate attributes. You do not need to use the Properties window to create data-driven tests. For example, you may wish to copy attributes from an existing data-driven test to quickly create another.

To access the data from within the unit test, use the DataRow property of the TestContext. For example, if you bound to a table with customer data, you could read the current customer's ID from the CustomerID column with the following:

     long customerID = TestContext.DataRow["CustomerID"]; 

Besides a column name, the DataRow property also accepts a column offset. If CustomerID were the first column in the table, you could supply a zero as the argument with the same result.

Because the unit test is run once per row in the data source, you want to be able to tell which rows caused certain results. Fortunately, this detail is already tracked for you. In the Test Results window, right-click on the test result and choose View Test Results Details. You will see a list of pass/fail results for each record in the database, enabling you to easily see which rows caused your test to fail.

Team System makes it very easy to write comprehensive tests against actual data. However, keep in mind that it is not enough to test only valid data, such as your real customer data. You will also want to have unit tests that verify your code's behavior when invalid data, perhaps in this case a negative number, is supplied.



Professional Visual Studio 2005 Team System
Professional Visual Studio 2005 Team System (Programmer to Programmer)
ISBN: 0764584367
EAN: 2147483647
Year: N/A
Pages: 220

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net