TDD: Testing Behavior in Abstract Classes

August 3, 2017 — Posted by Scott Bain

Interfaces vs. Abstract Classes

In languages like Java and C#, developers can use either an interface or an abstract class to create object polymorphism.  It’s a common question in technical training: “It is best to use an interface, or an abstract class?”

Furthermore, many teams adopt the “I” naming convention for interfaces; namely that an interface’s name should start with a capital I, whereas other classes (including abstract classes) should not.  The problem with this convention is that it creates design coupling.  Client objects that contain references to service objects must be changed when a simple, concrete class must be changed to become an abstraction, if an interface is to be used to model it.  Should client objects care whether a service is a concrete class, abstract class, or interface?  No.  This would seem to argue against this naming convention in the first place.

But the real problem stems from the fact that the “interface” type is commonly used for two very different purposes: to create polymorphism and to mark a class as a valid participant in a framework process.  For example, a class can implement “ISerializible”, not for casting purposes per se, but so it can be serialized by .Net or a similar framework.  This may be a tangential issue to the class’ core responsibility.  On the other hand, 10 different versions of a tax calculation algorithm implemented by 10 different tax calculation classes can all implement “ITaxCalc” so that they can be cast up and dealt with in the same way by various client classes.  This would create polymorphism around the central responsibility of all the classes involved: calculating taxes.  If we had started with a single algorithm, a concrete class called TaxCalc, and this was referred to across the system by that name, then when we evolve the system to support different algorithms and thus the class becomes an interface, then the type name would change (if the “I” convention is used) and all client code will have to be maintained.

Different Purposes, Different Approaches

It seems like a bad idea to use one idiom for two unrelated purposes.

Personally, I prefer to create polymorphism using abstract classes, and to mark a class for participation in a framework process using interfaces. 

Part of my argument is this:  when many different classes have a conceptual relationship, such as the tax calculators mentioned above, then it is likely they will also contain some code in their implementation that is the same.  This yields redundancy that creates maintenance problems when requirements due to tax laws and regulations change (for example).  An abstract class can implement common functionality, whereas interfaces cannot.  Even if a set of related classes contains no redundant implementation today, redundancies can emerge over time.  Abstract classes make this problem easy to solve whenever it arises.

Also, if I limit the use of interfaces to process flags, then the “I” convention is less of an issue.  I do not create design coupling within my system if I use it, because, for example, “IComparable” is not my interface, it allows a collection of classes to be sorted by a framework.  It belongs to that framework and is highly unlikely to be changed, due to the chaos this would create in everyone’s code if it were to be.  In any case, I don’t control its name.

TDD and Common Behaviors

If an abstract class is used to create polymorphism, and if there is indeed some common functionality in the base class, then the question arises: how do I test that behavior?  One cannot instantiate an abstract class, and thus its behavior cannot be triggered by a test unless that behavior is in a static method.  Static methods are disfavored for a number of reasons (I’ll deal with those in another blog), and I certainly would not make the behavior static just for testing purposes.  So what should a TDD practitioner, or a traditional tester, do about testing instance behavior that is implemented in an abstract class?

Here is a completely generic example:

Each “ConcreteService” version would have its own test, for each implementation version of the “VaryingFunction()” method.  But how would one write a test for the “CommonFunction()” method if it were an instance method, and one cannot create a instance?

Initially you might say “well, just pick any of the subclasses, create an instance of it and test the common function there, as they all have access to it.”  The problem is that this creates coupling in the test to the concrete service class that you arbitrarily chose.  If you happened to pick “ConcreteService1”, for instance, and later that class were to be retired/eliminated due to changing requirements, then the test of the common function would break even though that function is working fine.  Similarly, if "ConcreteService1" at some point in the future were to be changed to override the "CommonFunction()" method, this will also break the test.  We want tests that fail only for the reason we wrote them to.

Another Use for a Mock Object

Mock objects[1] are used to break and control dependencies in testing.  Here we can use a mock to eliminate coupling from the test of the common function to any of the concrete production classes.

This mock, like any subclass, has access through inheritance to the common function, but unlike other subclasses the mock:

  1. Is not part of the production code, but actually part of the test namespace/package/etc…
  2. Is never eliminated due to a changing requirement.  It is really part of the test.
  3. Is not a public class.  It is only visible to the tests.

Another advantage of this approach is that it makes it easier to test base-class behavior that is not exposed to the system in general (not public).

This is a pattern, a “Testing Class Adapter”[2].  It works because the test will hold the “Mock Service” by its concrete type, not in an upcast, and thus this new accessor method, which is public, can be called to access the protected method in the base class.  Again, this mock is not part of production, and thus does not break encapsulation in general, only for testing.

Summary

I prefer to use abstract classes to create polymorphism, and use interfaces to flag classes as participants in framework services.  When you do this, you can easily eliminate functional redundancies in derived classes by pushing them up into the base class.  To test this otherwise-redundant functionality use a mock object/testing class adapter to access it.

Patterns Training: http://www.netobjectives.com/training/design-patterns

TDD Training: http://www.netobjectives.com/training/sustainable-test-driven-development

---

[1] For more on Mock Objects see:

http://www.netobjectives.com/PatternRepository/index.php?title=TheMockObjectPattern

[2] For more on the Adapter Pattern see:

http://www.netobjectives.com/PatternRepository/index.php?title=TheAdapterPattern

 

Subscribe to our blog Net Objectives Thoughts Blog

Share this:

About the author | Scott Bain

Scott Bain is an consultant, trainer, and author who specializes in Test-Driven Development, Design Patterns, and Emergent Design.



        

Blog Authors

Al Shalloway
Business, Operations, Process, Sales, Agile Design and Patterns, Personal Development, Agile, Lean, SAFe, Kanban, Kanban Method, Scrum, Scrumban, XP
Cory Foy
Change Management, Innovation Games, Team Agility, Transitioning to Agile
Guy Beaver
Business and Strategy Development, Executive Management, Management, Operations, DevOps, Planning/Estimation, Change Management, Lean Implementation, Transitioning to Agile, Lean-Agile, Lean, SAFe, Kanban, Scrum
Israel Gat
Business and Strategy Development, DevOps, Lean Implementation, Agile, Lean, Kanban, Scrum
Jim Trott
Business and Strategy Development, Analysis and Design Methods, Change Management, Knowledge Management, Lean Implementation, Team Agility, Transitioning to Agile, Workflow, Technical Writing, Certifications, Coaching, Mentoring, Online Training, Professional Development, Agile, Lean-Agile, SAFe, Kanban
Ken Pugh
Agile Design and Patterns, Software Design, Design Patterns, C++, C#, Java, Technical Writing, TDD, ATDD, Certifications, Coaching, Mentoring, Professional Development, Agile, Lean-Agile, Lean, SAFe, Kanban, Kanban Method, Scrum, Scrumban, XP
Marc Danziger
Business and Strategy Development, Change Management, Team Agility, Online Communities, Promotional Initiatives, Sales and Marketing Collateral
Max Guernsey
Analysis and Design Methods, Planning/Estimation, Database Agility, Design Patterns, TDD, TDD Databases, ATDD, Lean-Agile, Scrum
Scott Bain
Analysis and Design Methods, Agile Design and Patterns, Software Design, Design Patterns, Technical Writing, TDD, Coaching, Mentoring, Online Training, Professional Development, Agile
Steve Thomas
Business and Strategy Development, Change Management, Lean Implementation, Team Agility, Transitioning to Agile
Tom Grant
Business and Strategy Development, Executive Management, Management, DevOps, Analyst, Analysis and Design Methods, Planning/Estimation, Innovation Games, Lean Implementation, Agile, Lean-Agile, Lean, Kanban