Unit Testing: Testing Properties
Visual Studio 2008 (Professional Edition and above) provides a really nice set of tools for development and execution of unit tests. This post provides suggestions for testing your property getters and setters.
[To begin with an overview of unit testing, start here.]
Let’s say your Customer class has a LastName property that looks like this:
In C#:
private string _LastName;
public string LastName
{
get { return _LastName; }
set
{
if (_LastName == null || _LastName != value)
{
string propertyName = "LastName";
// Perform any validation here
if (_LastName != value)
{
_LastName = value;
SetEntityState(EntityStateType.Modified,
propertyName);
}
}
}
}
(Someone just mentioned how all of those ending braces look like a flock of sea gulls! LOL!)
In VB:
Private _LastName As String
Public Property LastName() As String
Get
Return _LastName
End Get
Set(ByVal value As String)
If _LastName Is Nothing OrElse _
_LastName IsNot value Then
Dim propertyName As String = "LastName"
‘ Perform any validation
If _LastName IsNot value Then
_LastName = value
SetEntityState(EntityStateType.Modified, _
propertyName)
End If
End If
End Set
End Property
NOTE: The SetEntityState method used in this code is coming from the business object base class provided in this prior post.
The key step for developing a good unit test is to define the test scenarios.
Looking at the requirements, the following testing scenarios are required for the LastName property:
- Initial null value; set to null value (should perform validation but not set the dirty flag)
- Initial null value; set to valid string (should perform validation and set the dirty flag)
- Initial null value; set to empty string (should perform validation and set the dirty flag)
- Initial string value; set to null value (should perform validation and set the dirty flag)
- Initial string value, set to different string value (should perform validation and set the dirty flag)
- Initial string value, set to same string value (should not perform validation and not set the dirty flag)
- Initial string value, set to empty value (should perform validation and set the dirty flag)
And if you have validation code, you will have more scenarios to test valid and invalid values. But this is enough to give you the general idea.
If you are building the unit test from existing code, you can generate the basic structure of the unit test for the property following the techniques detailed in this prior post.
But the test generation only gives you the basic template for testing your properties. This post focuses on how to update that template to perform the required unit testing.
Visual Studio will generate the following unit test template for the LastName property that was shown at the beginning of this post:
In C#:
/// <summary>
///A test for LastName
///</summary>
[TestMethod()]
public void LastNameTest()
{
Customer target = new Customer(); // TODO: Initialize to an appropriate value
string expected = string.Empty; // TODO: Initialize to an appropriate value
string actual;
target.LastName = expected;
actual = target.LastName;
Assert.AreEqual(expected, actual);
Assert.Inconclusive("Verify the correctness of this test method.");
}
In VB:
”'<summary>
”’A test for LastName
”'</summary>
<TestMethod()> _
Public Sub LastNameTest()
Dim target As Customer = New Customer ‘ TODO: Initialize to an appropriate value
Dim expected As String = String.Empty ‘ TODO: Initialize to an appropriate value
Dim actual As String
target.LastName = expected
actual = target.LastName
Assert.AreEqual(expected, actual)
Assert.Inconclusive("Verify the correctness of this test method.")
End Sub
To cover the defined testing scenarios, this code needs to be enhanced as follows:
In C#:
/// <summary>
///A test for LastName
///</summary>
[TestMethod()]
public void LastNameTest()
{
Customer target;
string expected;
string actual;
// Null to Null
target = new Customer();
expected = null;
target.LastName = expected;
actual = target.LastName;
Assert.AreEqual(expected, actual, "Values are not equal");
Assert.AreEqual(false, target.IsDirty,
"Object not marked as dirty");
// Null to value
target = new Customer();
expected = "Johnson";
target.LastName = expected;
actual = target.LastName;
Assert.AreEqual(expected, actual, "Values are not equal");
Assert.AreEqual(true, target.IsDirty, _
"Object not marked as dirty");
// Null to Empty
target = new Customer();
expected = string.Empty;
target.LastName = expected;
actual = target.LastName;
Assert.AreEqual(expected, actual, "Values are not equal");
Assert.AreEqual(true, target.IsDirty,
"Object not marked as dirty");
// Value to Null
target = new Customer() {LastName = "Johnson"};
target.SetEntityState(BoBase.EntityStateType.Unchanged);
expected = null;
target.LastName = expected;
actual = target.LastName;
Assert.AreEqual(expected, actual, "Values are not equal");
Assert.AreEqual(true, target.IsDirty,
"Object not marked as dirty");
// Value to new Value
target = new Customer() {LastName = "Johnson"};
target.SetEntityState(BoBase.EntityStateType.Unchanged);
expected = "Jones";
target.LastName = expected;
actual = target.LastName;
Assert.AreEqual(expected, actual, "Values are not equal");
Assert.AreEqual(true, target.IsDirty,
"Object not marked as dirty");
// Value to same Value
target = new Customer() {LastName = "Johnson"};
target.SetEntityState(BoBase.EntityStateType.Unchanged);
expected = "Johnson";
target.LastName = expected;
actual = target.LastName;
Assert.AreEqual(expected, actual, "Values are not equal");
Assert.AreEqual(false, target.IsDirty,
"Object not marked as dirty");
// Value to Empty
target = new Customer() {LastName = "Johnson"};
target.SetEntityState(BoBase.EntityStateType.Unchanged);
expected = string.Empty;
target.LastName = expected;
actual = target.LastName;
Assert.AreEqual(expected, actual, "Values are not equal");
Assert.AreEqual(true, target.IsDirty,
"Object not marked as dirty");
}
In VB:
”'<summary>
”’A test for LastName
”'</summary>
<TestMethod()> _
Public Sub LastNameTest()
Dim target As Customer
Dim expected As String
Dim actual As String
‘ Null to Null
target = New Customer
expected = Nothing
target.LastName = expected
actual = target.LastName
Assert.AreEqual(expected, actual, "Values are not equal")
Assert.AreEqual(False, target.IsDirty, _
"Object not marked as dirty")
‘ Null to value
target = New Customer
expected = "Johnson"
target.LastName = expected
actual = target.LastName
Assert.AreEqual(expected, actual, "Values are not equal")
Assert.AreEqual(True, target.IsDirty, _
"Object not marked as dirty")
‘ Null to Empty
target = New Customer
expected = String.Empty
target.LastName = expected
actual = target.LastName
Assert.AreEqual(expected, actual, "Values are not equal")
Assert.AreEqual(True, target.IsDirty, _
"Object not marked as dirty")
‘ Value to Null
target = New Customer With {.LastName = "Johnson"}
target.SetEntityState(BOBase.EntityStateType.Unchanged)
expected = Nothing
target.LastName = expected
actual = target.LastName
Assert.AreEqual(expected, actual, "Values are not equal")
Assert.AreEqual(True, target.IsDirty, _
"Object not marked as dirty")
‘ Value to new Value
target = New Customer With {.LastName = "Johnson"}
target.SetEntityState(BOBase.EntityStateType.Unchanged)
expected = "Jones"
target.LastName = expected
actual = target.LastName
Assert.AreEqual(expected, actual, "Values are not equal")
Assert.AreEqual(True, target.IsDirty, _
"Object not marked as dirty")
‘ Value to same Value
target = New Customer With {.LastName = "Johnson"}
target.SetEntityState(BOBase.EntityStateType.Unchanged)
expected = "Johnson"
target.LastName = expected
actual = target.LastName
Assert.AreEqual(expected, actual, "Values are not equal")
Assert.AreEqual(False, target.IsDirty, _
"Object not marked as dirty")
‘ Value to Empty
target = New Customer With {.LastName = "Johnson"}
expected = String.Empty
target.LastName = expected
actual = target.LastName
Assert.AreEqual(expected, actual, "Values are not equal")
Assert.AreEqual(True, target.IsDirty, _
"Object not marked as dirty")
End Sub
NOTE: The SetEntityState method and IsDirty property used in this code are coming from the business object base class provided in this prior post.
NOTE: You may get an error when calling SetEntityState because in the business object base class is defined to be protected internal (protected friend in VB), not public. If so, you need to use the technique presented here to allow your tests to access internal/friend properties and methods.
WOW! That is a LOT of test code! Some experts have said that every line of code needs at least 3 – 5 lines of test code.
We have 17 lines of C# code and 15 lines of VB code in our property procedure. If I counted correctly, there are 51 lines of C# test code and 50 lines of VB test code. So that is about 3x the number of code lines.
Let’s walk through what this test code is doing, following through with the testing scenarios provided earlier in this post.
Scenario 1: The code creates a new instance of the Customer class. By default, a new instance sets any string values to null/nothing. So the test code simply sets the property to null/nothing, gets the value, confirms that the value is as expected, and ensures that it did not get marked as dirty.
Scenario 2: The code again creates a new instance of the Customer class. By default a new instance sets any string values to null/nothing. So the test code sets the property to a value, gets the value, confirms that the value is as expected, and ensures that the object was marked as dirty.
Scenario 3: The code again creates a new instance of the Customer class. By default a new instance sets any string values to null/nothing. So the test code sets the property to an empty string, gets the value, confirms that the value is as expected, and ensures that the object was marked as dirty.
Now for the harder scenarios. There is no easy way to set a non-null initial value for a property. One option is to set the private backing variable to the desired initial value. But with the backing variable being private, there is extra code to write to make it work.
Another option is to set the property using the setter. But this marks the object as dirty, adversely interfering with the test. So if you use this technique, you then need to call SetEntityState to clear the entity state and ensure it is not marked as dirty. The sample code used this technique.
Scenario 4: The code creates a new instance of the customer class, setting the initial value of the property to a valid value. It then calls SetEntityState to clear the dirty flag. Then the test code sets the property to a null, gets the value, confirms that the value is as expected, and ensures that the object was marked as dirty.
Scenario 5: The code creates a new instance of the customer class, setting the initial value of the property to a valid value. It then calls SetEntityState to clear the dirty flag. Then the test code sets the property to another valid value, gets the value, confirms that the value is as expected, and ensures that the object was marked as dirty.
Scenario 6: The code creates a new instance of the customer class, setting the initial value of the property to a valid value. It then calls SetEntityState to clear the dirty flag. Then the test code sets the property to the same value, gets the value, confirms that the value is as expected, and ensures that the object was not marked as dirty.
Scenario 7: The code creates a new instance of the customer class, setting the initial value of the property to a valid value. It then calls SetEntityState to clear the dirty flag. Then the test code sets the property to an empty string, gets the value, confirms that the value is as expected, and ensures that the object was marked as dirty.
If the field was validated, such as required field validation or a maximum length check, additional scenarios and associated test code would be required.
So for every business object property in your application, define the appropriate set of test scenarios and build the test code to support each scenario.
Or build a test base class that performs the basic set of tests for each of your properties. But that is left for a future post.
Enjoy!
Christophe Lambrechts — November 7, 2011 @ 6:39 am
Shouldn’t you use something as a Data driven Unit Test for this kind of situation. MSDN reference: http://msdn.microsoft.com/en-us/library/ms182527.aspx
Or at least I would advice to use a kind of function, you have 7 times almost the same code in your file. This is not a very good programming practice.
DeborahK — November 7, 2011 @ 1:09 pm
Hi Christophe –
Yes, in our *real* applications we have a function that handles this. (See the last paragraph of the post.)
The purpose of this post was to think through how a property could be unit tested to give the reader an idea of the thought process and the types of things to check. The post did not include the “refactored” code showing a base class or set of general functions.
Thanks for your comments!