We previously looked at how we can run a single written test with multiple data. By adding parameters and using the TestCase
attribute, we were able to convert one test into many (from the test runner’s point of view) without explicitly writing more code. In this week’s article, we’ll take this concept and apply it while zoomed out – instead of substituting test arguments, we’ll be interchanging object types.
An Example Use Case
Let’s assume we’re building a record-keeping system for a university. We want to be able to store details for the staff and students who attend. When designing the data model, we add the following classes. They each have properties to capture the first and last names of the people they represent. The two names are kept as separate data entities for maximum flexibility, but each class also has a method to consistently combine them together. To avoid over-complicating this example, each class only has one unique distinguishing property.
public class Student
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string CourseProgramme { get; set; }
public string GetFullName()
{
return $"{FirstName} {LastName}";
}
}
public class Staff
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Department { get; set; }
public string GetFullName()
{
return $"{FirstName} {LastName}";
}
}
We’ve avoided using class inheritance as we want to keep the data models simple and flexible. However, there will be use cases where we’ll want to process records regardless of their type. To achieve this, we add the following interface.
public interface IPerson
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string GetFullName();
}
And apply it in Student
and Staff
.
public class Student : IPerson
{
...
public class Staff : IPerson
{
...
This introduces one small problem: the implementation of GetFullName
is duplicated across both classes. In this case, having identical copies of the code isn’t the issue: the logic is trivial, so we aren’t too concerned about maintenance (e.g. if we wanted the full name to be formatted differently). We do however want both implementations to produce consistent results.
Luckily, we don’t need to achieve this by having a single piece of logic to combine first and last names. (There’s nothing to stop us from doing so, but the overhead outweighs the benefit in this situation.) Instead, we can write tests to check GetFullName
generates the same output when first and last names are set appropriately in both classes.
Writing the Test
Let’s start by writing a test for Student. In the following example, we compare the result of GetFullName
with an expected value.
public class GetFullNameTests
{
[Test]
public void FullNameContainsFirstAndLastName()
{
// Arrange
var person = new Student
{
FirstName = "First",
LastName = "Last"
};
// Act
var fullName = person.GetFullName();
// Assert
Assert.That(fullName, Is.EqualTo("First Last"));
}
}
We want GetFullName
to have the same behaviour in the Staff
class too (though the way it’s achieved doesn’t need to be a carbon copy). One of the most obvious ways to add coverage for this would be to copy and paste the test in the previous example, changing Student
for Staff
; this would result in two almost identical tests. However, our code would be tidier if we could parameterise the object type we want created – especially as we’re checking for the same behaviour.
Generic test fixtures let us do this. As shown in the following example, we specify the desired type as an argument for the TestFixture
attribute we decorate our test class with. You might notice we haven’t used the TestFixture
attribute until now. That’s because it has been optional since NUnit 2.5, except for generic or parameterised tests.
[TestFixture(typeof(Staff))]
[TestFixture(typeof(Student))]
public class GetFullNameTests<T> where T : IPerson, new()
{
[Test]
public void FullNameContainsFirstAndLastName()
{
// Arrange
var person = new T
{
FirstName = "First",
LastName = "Last"
};
// Act
var fullName = person.GetFullName();
// Assert
Assert.That(fullName, Is.EqualTo("First Last"));
}
}
Summary
You sometimes write tests for identical behaviour across multiple classes. Where there’s a common type, you might be able to do this with generic test fixtures instead of duplicating individual tests.
A generic test class can have its data type/s specified by passing them as arguments when applying the TestFixture
attribute. Depending on the test, you might be able to use this technique to verify certain behaviours across different classes without explicitly writing a test for each data type. In turn, you’ll be able to expand your test coverage quickly, cleanly, and with relatively little effort.
Thanks for reading!
This article is from my newsletter. If you found it useful, please consider subscribing. You’ll get more articles like this delivered straight to your inbox (once per week), plus bonus developer tips too!
Top comments (0)