Writing Unit Tests
After creating the test plan, you are now ready to start developing your unit tests.
Unit Test Classes
Declaring a unit test class is similar to declaring a regular class, but it includes the [TestClass]
attribute. The use of the [TestClass]
attribute is what differentiates a test class from just a regular class.
Danger
If your unit test class is missing the [TestClass]
attribute, the Test Explorer will not recognize the class as containing unit tests.
Test Class Naming Convention
Unit test class identifiers are named by using the class name of the class you are testing followed by the word "Tests". The test class above is testing a class named Person
, because the test class identifier is PersonTests
.
Unit Test Methods
You will code unit test methods within a class with the [TestClass]
attribute. As a general guideline, you will write one unit test method per test case in your test plan.
Unit test methods are declared and defined like other methods. The use of the [TestMethod]
attribute denotes the method is a unit test method.
namespace ADEV.UnitTesting
{
[TestClass]
public class PersonTests
{
[TestMethod]
public void TestMethod1()
{
}
}
}
Danger
If your unit test method is missing the [TestMethod]
attribute, the Test Explorer will not recognize the method as a unit tests method.
Test Method Naming Convention
Unit test method identifiers will follow this general naming format:
Where each part of the test method identifier means:
- The name of the method being tested.
- The scenario under which it's being tested.
- The expected behavior when the scenario is invoked.
Example:
namespace ADEV.UnitTesting
{
[TestClass]
public class PersonTests
{
[TestMethod]
public void Constructor_NameIsNull_ThrowsException()
{
}
}
}
Test Method Organization
Test methods in your test class should be ordered by the unit being tested. You will not want to organize your tests based on the outcome being tested.
AAA Pattern
The AAA (Arrange, Act, Assert) pattern is a common pattern for writing unit test methods.
- The Arrange part of the unit test defines test data and initializes an object used for the test.
- The Act part of the test invokes the unit being tested under the conditions of the test case.
- The Assert part of the test verifies that the unit being tested behaved as expected.
One of the most important aspects of testing, especially coding unit tests, is readability. The AAA pattern clearly separates what is being tested from the arrange and assert parts of the test. Separating the statements for these parts of the tests highlights the dependencies to invoke the method being tested, how it's being called and what you are trying to verify.
Assert Class
The Assert
class contains a collection of methods to determine the result of a test. If the condition being tested is not met, an AssertFailedException
is thrown.
Common Assert Class Method
- AreEqual(Object, Object) - Tests whether the specified objects are equal and throws an exception if the two objects are not equal.
- AreNotEqual(Object, Object) - Tests whether the specified values are unequal and throws an exception if the two values are equal.
- IsTrue(Boolean) - Tests whether the specified condition is
true
and throws an exception if the condition isfalse
. - IsFalse(Boolean) - Tests whether the specified condition is
false
and throws an exception if the condition istrue
. - IsNull(Object) - Tests whether the specified object is null and throws an exception if it is not.
- IsNotNull(Object) - Tests whether the specified object is non-null and throws an exception if it is null.
- AreSame(Object, Object) - Tests whether the specified objects both refer to the same object and throws an exception if the two inputs do not refer to the same object.
- AreNotSame(Object, Object) - Tests whether the specified objects refer to different objects and throws an exception if the two inputs refer to the same object.
- ThrowsException<T>() - Tests whether the code specified by the delegate action throws exact given exception of type
T
(and not of derived type) and throwsAssertFailedException
if code does not throws exception or throws exception of type other thanT
. This method returns a reference to the exception the exception if one is thrown.
Note
All Assert
methods will throw an AssertFailedException
when the condition is not met. The result of the test is a fail.
Reflection
Many of the tests you will develop will involve verifying the state of an object. State is stored in private
fields in the class. To verify test cases where the result deals with object state, you will need to use a concept called Reflection. Reflection is the ability to retrieve data that is normally not accessible at run-time.
In the testing you did prior to this topic, you would have normally called an accessor method to verify changes to state. This is no longer satisfactory, as one of your unit testing goals is to isolate the unit your are testing. This means you do not want to invoke other units of the class while testing a specific unit.
PrivateObject Class
The PrivateObject
class represents a public
"version" of an object. The class contains methods to accesses private
fields, methods, and properties.
To gain access to an object's private
members, construct an instance of PrivateObject
, initializing it with a reference to the object you wish access its private
members.
Person person = new Person(name, amountOfMoney);
PrivateObject target = new PrivateObject(person);
Use the following methods of the PrivateObject
class:
- GetField(String) : Object - Returns the value of the specified field.
- SetField(String, Object) : void - Sets the specified field to the specified value.
- Invoke(String, Object[]) : Object - Invokes the specified method. The
Object[]
represents the arguments. Returns the result of the method.
PrivateObject target = new PrivateObject(person);
decimal actual = (decimal)target.GetField("amountOfMoney");
SetField Method
The SetField
method is often used during the arrange part of a unit test when the state cannot be initialized to setup the test using the constructor of the class.
Good:
[TestMethod]
public void Withdraw_AmountGreaterThanZero_UpdatesBalance()
{
// Arrange
BankAccount account;
// Initializes balance to zero
account = new BankAccount();
PrivateObject target;
target = new PrivateObject(account);
// Initialize balance
target.SetField("balance", 1000)
// Act
// ...
// Assert
// ...
}
Bad:
[TestMethod]
public void Withdraw_AmountGreaterThanZero_UpdatesBalance()
{
// Arrange
BankAccount account;
// Initializes balance to zero
account = new BankAccount();
account.Balance = 1000;
// Act
// ...
// Assert
// ...
}
The sample code above is not ideal because it is invoking another unit in the class, potentially affecting the results of the unit being tested.
Testing Abstract Class
Due to the inability of directly instantiating instances of an abstract class, you can't directly unit test them. Therefore, inherited properties and methods are tested in a derived class. Fields of a class' base class can be accessed using the PrivateType
class.
PrivateType Class
The PrivateType
class is used like PrivateObject
, and can also be used for testing Static Classes.
Best Practices For Writing Tests
Before you really dive into developing your unit tests, keep the following best practices in mind:
- Avoid logic in your tests. This includes selections and loops.
- A unit is tested in isolation of other units in the class (with the exception of the constructor).
- Always test constructor methods first.
- Complete all the tests for a unit before moving onto another unit.
- Group tests in your test class by unit, not by test type.
- Keep the code in your unit test method simple.
- Try to order your tests in order of dependency (when known or possible).
- Prefer helper methods to setup and teardown.
- Avoid multiple acts.
- No unit is too insignificant to test.
Test Method Examples
The following unit test method examples will follow the sample test plan. Not every test case will be demonstrated here, as some of the test cases will produce similar unit test method implementation.
- Constructor Tests
- Auto-implemented Properties
- Property Tests
- Method Tests
- Accessing Base Class Fields