How to Test Abstract Classes

Abstract classes typically offer one or more concrete methods. These must be tested as well. There are several ways how to do it, but which one to choose?


Consider an abstract class with a concrete method (Java):

abstract class Person {

    protected final String firstName;
    protected final String lastName;

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String fullName() {
        return String.format("%s %s", firstName, lastName);
    }
    
    public abstract String greeting();
}

What are the possibilities to test such a class?

Anonymous Classes

We can create an ad-hoc instance of the abstract class and stub the abstract method which are not under test:

@Test
void full_name_is_provided() {
    Person person = new Person("John", "Smith") {
        @Override
        public String greeting() {
            return null;
        }
    };
    assertEquals("John Smith", person.fullName());
}

But such a code is pretty cumbersome and hard to read. Just image a class with multiple abstract methods, the test code becomes longer and longer and breaks one of the main property of a good test - readability.

Another problem is the coupling between the test and implementation. Even that the method is marked as final, the code should focus only on the contract and not on such implementation details as details can change in the future and a good test should be resistant to refactoring.

Creating an anonymous class doesn't seem to be the best option. Can we do better?

Mocking

This is probably the first advice you get if you search on the Internet. With mocking everything is easy (Mockito):

@Test
void full_name_is_provided() {
    Person person = mock(Person.class, withSettings()
            .useConstructor("John", "Smith")
            .defaultAnswer(CALLS_REAL_METHODS));

    assertEquals("John Smith", person.fullName());
}

Mocking an abstract class is practically just like creating an anonymous class but using convenient tools. It has the same drawbacks and, again, it's probably not the best option we have.

Concrete Class

An abstract class makes actually no sense without being extended with a concrete class. A concrete class is where the requirements must be met.

class Sailor extends Person {

    public Sailor(String firstName, String lastName) {
        super(firstName, lastName);
    }

    @Override
    public String greeting() {
        return "Ahoy!";
    }
}

This is the right place for testing:

class SailorTest {

    @Test
    void full_name_is_provided() {
        Sailor sailor = new Sailor("James", "Cook");

        assertEquals("James Cook", sailor.fullName());
    }

    @Test
    void greeting_is_provided() {
        Sailor sailor = new Sailor("James", "Cook");

        assertEquals("Ahoy!", sailor.greeting());
    }
}

Of course, there will be probably some duplicates in the test code when we have more concrete classes extending the abstract class. But the price is still smaller that losing the value of the test, when the acceptance depends on the inherited (potentially unknown) implementation. All in all, the inherited implementation can and often does (even it should be avoided as much as possible) change in the concrete classes and must be tested anyway.

True Object-Oriented

We can face a situation, for instance when working on a util library, where the provided default implementation is much more complicated that in our simple fullName() method. Such a case tell us that we are probably doing too much in the class. The solution is to introduce a new class and extract the functionality into it:

class Name {

    private final String first;
    private final String last;

    public Name(String first, String last) {
        this.first = first;
        this.last = last;
    }
    
    public String full() {
        return String.format("%s %s", firstName, lastName);
    }
}

abstract class Person {

    protected final Name name;

    public Person(String firstName, String lastName) {
        this.name = new Name(firstName, lastName);
    }

    public final String fullName() {
        return name.full();
    }
    
    public abstract String greeting();
}

We moved the functionality into a specialized concrete class, easy to test. Now we can ensure the default implementation of the abstract class works as expected.

Conclusion

Implementation inheritance is in general not the best practice as it tightly couples children classes with the parent class. Composition should always be preferred over inheritance. However, there are cases where inheritance makes sense. In such cases testing must be very careful.

Business is always concrete. Relying blindly on testing of an abstract class could break concrete requirements. Test always a concrete class as the concrete class must satisfy the business requirements.

Happy testing!