Sign Up |  Register

All tags

Testing with Visual Studio 2012: How to write good unit tests

08.08.20131996 просм.

A lot has been written about what makes a good unit test, and we don’t have space to replicate it allhere. If you’re looking for more information, search the web for “unit test patterns.” However, there are some particularly useful tips.

Arrange – Act – Assert
The general form {Arrange; Act; Assert} is favored by many developers:
Arrange: Set up test data;
Act: Call the unit under test;
Assert: Compare the expected and actual results, and
log the result of the comparison as a fail or pass.
For example:
C#
[TestMethod]
public void TestSortByFlavor()
{
// Arrange: Set up test data:
var catalog = new IceCreamCatalog(Flavor.Oatmeal, Flavor.Cheese);
// Act: Exercise the unit under test:
catalog.SortByFlavor();
// Assert: Verify and log the result:
Assert.AreEqual(Flavor.Cheese, catalog.Items[0].Flavor);
}

Test one thing with each test

Don’t be tempted to make one test method exercise more than one aspect of the unit’s behavior. Doing so leads to tests that are difficult to read and update. It can also lead to confusion when you are interpreting a failure.

Keep in mind that the MSTest test framework does not by default guarantee a specific ordering for the tests, so you cannot transfer state from one test to another. However, in Visual Studio, you can use the Ordered Test feature to impose a specific sequence.

Use test classes to verify different behavioral areas of the code under test

Separate tests for different behavioral features into different test classes. This usually works well because you need different shared utility methods to test different features. If you want to share some methods between test classes, you can of course derive them from a common abstract class.

Each test class can have a TestInitialize and TestCleanup method. (These are the MSTest attributes; there are equivalents in other test frameworks, typically named Setup and Teardown.) Use the initialize or setup method to perform the common tasks that are always required to set up the starting conditions for a unit test, such as creating an object to test, opening the database or connections, or loading data. The cleanup or teardown method is always called, even if a test method fails; this is a valuable feature that saves you having to write all your test code inside try…finally blocks.

Test exception handling

Test that the correct exceptions are thrown for invalid actions or inputs. You could use the [ExpectedException] attribute, but be aware that a test with that attribute will pass no matter what statement in the test raises an exception.
A more reliable way to test for exceptions is shown here:

C#
[TestMethod, Timeout(2000)]
public void TestMethod1()
{ …
AssertThrows<InvalidOperationException>( delegate
{
MethodUnderTest();
});
}
internal static void AssertThrows<exception>(Action method)
where exception : Exception
{
try
{
method.Invoke();
}
catch (exception)
{
return; // Expected exception.
}
catch (Exception ex)
{
Assert.Fail(“Wrong exception thrown: ” + ex.Message);
}
Assert.Fail(“No exception thrown”);
}

A function similar to AssertThrows is built into many testing frameworks.

Don’t only test one input value or state

By verifying that 2.0==MySquareRootFunction(4.0), you haven’t truly verified that the function works for all values. The code coverage tool might show that all your code has been exercised, but it might still be the case that other inputs, or other starting states, or other sequences of inputs, give the wrong results. Therefore, you should test a representative set of inputs, starting states, and sequences of action.

Look for boundary cases: those where there are special values or special relationships between the values. Test the boundary cases, and test representative values between the boundaries. For example, inputs of 0 and 1 might be considered boundary cases for a square root function, because there the input and output values are equal. So test, for example, -10, -1, -0.5, 0, 0.5, 1, and 10.

Test also across the range. If your function should work for inputs up to 4096, try 4095 and 4097. The science of model-driven testing divides the space of inputs and states by these boundaries, and seeks to generate test data accordingly.

For objects more complex than a simple numeric function, you need to consider relationships between different states and values: for example, between a list and an index of the list.

Separate test data generation from verification

A postcondition is a Boolean expression that should always be true of the input and output values, or the starting and ending states of a method under test. To test a simple function, you could write:

C#
[TestMethod]
public void TestValueRange()
{
while (GenerateDataForThisMethod(
out string startState, out double inputValue)))
{
TestOneValue(startState, inputValue);
}
}
// Parameterized test method:
public void TestOneValue(string startState, double inputValue)
{
// Arrange – Set up the initial state:
objectUnderTest.SetKey(startState);
// Act – Exercise the method under test:
var outputValue = objectUnderTest.MethodUnderTest(inputValue);
// Assert – Verify the outcome:
Assert.IsTrue(PostConditionForThisMethod(inputValue, outputValue));
}
// Verify the relationship between input and output values and states:
private bool
PostConditionForThisMethod
(string startState, double inputValue, double outputValue)
{
// Accept any of a range of results within specific constraints:
return startState.Length>0 && startState[0] == ‘+’
? outputValue > inputValue
: inputValue < outputValue;
}

To test an object that has internal state, the equivalent test would set up a starting state from the test data, and call the method under test, and then invoke the postcondition to compare the starting and ending states.

The advantages of this separation are:

  • The postcondition directly represents the actual requirement, and can be considered separately from issues of what data points to test.
  • The postcondition can accept a range of values; you don’t have to specify a single right answer for each input.
  • The test data generator can be adjusted separately from the postcondition. The most important requirement on the data generator is that it should generate inputs (or states) that are distributed around the boundary values.

Pex generates test data

Take a look at Pex, which is an add-in for Visual Studio. There’s also a standalone online version.

Pex automatically generates test data that provides high code coverage. By inspecting the code under test, Pex generates interesting input-output values. You provide it with the parameterized version of the test method, and it generates test methods that invoke the test with different values. (The website also talks about Moles, an add-in for Visual Studio 2010, which has been replaced by an integrated feature, fakes, for Visual Studio 2012.)

Tags: , , , ,

Leave a Reply

You must be logged in to post a comment.

Партнеры DevOpsHub и DevOpsWiki