How to Unit Test an Abstract Class
How to unit test an abstract class? Or a whole class hierarchy that depends on that abstract class? Let’s see.
1. Unit testing an abstract class
Imagine you work on a people software in a university and have the following code:
public class Student
{
public string Name { get; set; }
public void EnrollInCourse(Course course)
{
/* ... */
}
public string GetSignature()
{
return $"Best regards,\r\n{Name},\r\nStudent at MIT";
}
}
public class Professor
{
public string Name { get; set; }
public void ApplyForFacultyJob(Faculty faculty)
{
/* ... */
}
public string GetSignature()
{
return $"Best regards,\r\n{Name},\r\nProfessor at MIT";
}
}
In this example, Student
and Professor
share common functionality, and so you decide to extract it into a Person
abstract base class:
public class Student : Person
{
public void EnrollInCourse(Course course)
{
/* ... */
}
protected override string GetTitle() => "Student";
}
public class Professor : Person
{
public void ApplyForFacultyJob(Faculty faculty)
{
/* ... */
}
protected override string GetTitle() => "Professor";
}
public abstract class Person
{
public string Name { get; set; }
protected abstract string GetTitle();
public string GetSignature()
{
return $"Best regards,\r\n{Name},\r\n{GetTitle()} at MIT";
}
}
This version looks much better because it follows the DRY principle.
Now, the question is — how to test it?
There are two options:
-
Unit test all classes — When you test each class separately (
Student
,Professor
, andPerson
) -
Unit test only concrete classes — When you test only the non-abstract classes (
Student
andProfessor
)
Let’s discuss them separately.
2. Test class per each production class
Testing each class in the hierarchy provides the benefit of not repeating your tests. Both Student
and Professor
derive the GetSignature()
method from the base Person
class, and so it makes sense to cover that base class directly.
Of course, you can’t instantiate an abstract class, but you can create a mock on top of it, and ask that mock to reuse the non-abstract methods of that class. With Moq, you can do it like this:
[Fact]
public void Every_person_has_a_signature()
{
// Arrange
var mock = new Mock<Person>()
{
CallBase = true /* the "reuse non-abstract" part */
};
mock.Setup(x => x.GetTitle()).Returns("Assistant");
mock.Object.Name = "Alice";
// Act
string signature = mock.Object.GetSignature();
// Assert
Assert.Equal("Best regards,\r\nAlice,\r\nAssistant at MIT", signature);
}
Overall, you will get one test class per each class in the hierarchy:
public class PersonTests
{
[Fact]
public void Every_person_has_a_signature()
{
/* ... */
}
/* Other tests specific to Person */
}
public class StudentTests
{
/* Tests specific to Student */
}
public class ProfessorTests
{
/* Tests specific to Professor */
}
I’ll call this approach test class per each production class.
3. Test class per concrete production class
The second approach is to test only the concrete classes in the hierarchy: Student
and Professor
. In this case, you will only have two test classes, StudentTests
and ProfessorTests
, but you will have to duplicate tests related to the GetSignature()
method.
So, which approach is better and why?
The answer is: always test only concrete classes; don’t test abstract classes directly. The reason is that abstract classes are implementation details. From the client perspective, it doesn’t matter how Student
or Professor
implement their GetSignature()
methods. They could derive it from a base class like in our case, but they could also just implement their own copy of it.
Targeting tests at the abstract base class binds them to the code’s implementation details. This, in turn, makes those tests fragile.
The fragility becomes self-evident when you start to customize the base class’s functionality. For example, let’s say that we want each derived class to have its own closing phrase, not just "Regards"
. For that, we introduce another abstract method:
public abstract class Person
{
public string Name { get; set; }
protected abstract string GetPosition();
protected abstract string GetClosingPhrase(); /* The new abstract method */
public string GetSignature()
{
return $"{GetClosingPhrase()},\r\n{Name},\r\n{GetPosition()} at MIT";
}
}
public class Student : Person
{
protected override string GetPosition() => "Student";
protected override string GetClosingPhrase() => "Best regards";
}
public class Professor : Person
{
protected override string GetPosition() => "Professor";
protected override string GetClosingPhrase() => "Best regards";
}
Notice that Student
and Professor
still have their closing phrase as "Best regards"
, so the observable behavior of these classes didn’t change. But the test has broken anyway. We have to add the additional setup line in order to fix the test:
[Fact]
public void Every_person_has_a_signature()
{
// Arrange
var mock = new Mock<Person>()
{
CallBase = true
};
/* the additional setup */
mock.Setup(x => x.GetClosingPhrase()).Returns("Best regards");
mock.Setup(x => x.GetTitle()).Returns("Assistant");
mock.Object.Name = "Alice";
// Act
string signature = mock.Object.GetSignature();
// Assert
Assert.Equal("Best regards,\r\nAlice,\r\nAssistant at MIT", signature);
}
Unit testing abstract classes leads to the same consequences as unit testing private methods. In fact, these two practices are essentially the same anti-pattern. Both couple your tests to implementation details and therefore increase the noise you have to deal with after each refactoring.
Here’s the same code sample represented with a private method:
/* Unit test these... */
public string GetStudentSignature(string name)
{
return GetSignature(name, "Best regards", "Student");
}
public string GetProfessorSignature(string name)
{
return GetSignature(name, "Best regards", "Professor");
}
/* ... but not this */
private string GetSignature(string name, string closingPhrase, string position)
{
return $"{closingPhrase},\r\n{name},\r\n{position} at MIT";
}
You wouldn’t unit test the private GetSignature()
method, would you? Nor should you test abstract base classes.
So, the approach to choose here is test class per concrete production class, where you create a test class per each concrete class of the hierarchy.
But doesn’t it lead to test duplication, you might ask? It does. It’s not a bad thing, though. The Student
and Professor
aren’t the same class, and should be tested separately. The fact that they share some common functionality is just an implementation detail, which should be of no concern to your tests. Your tests should always view the system under test as a black box.
4. Summary
-
There are two ways to unit test a class hierarchy and an abstract class:
-
Using a test class per each production class
-
Using a test class per concrete production class
-
-
Choose the test class per concrete production class approach; don’t unit test abstract classes directly
-
Abstract classes are implementation details, similar to private methods
Subscribe
Comments
comments powered by Disqus