Click here to Skip to main content
Click here to Skip to main content

Fun with Unit Testing - testing abstract classes

, 23 Sep 2003
Rate this:
Please Sign up or sign in to vote.
I'll show how to use mock objects to test abstract classes.

Overview

In this article I’ll be looking into a trickier part of unit testing – testing the functionality of classes which cannot be instantiated on their own, abstract classes. I’ll show how to overcome this obstacle using “Mock Objects”, a technique which has a whole methodology behind it, all on its own. We’ll see how mock objects provide us with abilities that let us query what’s happening inside our derived class. You can also find more material on my weblog.

Introduction

Unit testing is all good and well as a theoretical nirvana. “Yeah, we do unit tests” is a great answer to have when someone asks you, but achieving this is a process just like any other software development process. The overall idea looks great, but you come across problems that don’t fit your original plans, or things you can’t deal with the way you’re used to. Unit testing has several of these obstacles routed at the core of the mythology. One of those obstacles is the testing of objects that cannot be instantiated on their own (abstract classes are one of several manifestations of this problem). As I’ll demonstrate, this too is possible using mock objects, objects that exist for the sole purpose of helping us in our task of testing.

The problem with abstract classes

So what’s the definition of the problem we are facing? Well, to run a test against a class, we need to have an instance of that class to work on. We need something to call methods on and get values back if needed. We need a real live object. In the case of abstract classes there’s no way to do this.

We could make the class non-abstract for the sole purpose of testing it, but that would violate one of the most important concepts of unit tests – they should not alter the behavior or the data of the tested application. Making changes to the design of an object model for no reason other than tests, is not the course of action we want to take.

We need to ask ourselves what we want to test in an abstract class. Usually abstract classes contain the plumbing required by classes that will derive from them. What we want to know is if the derived classes, as clients to the services the abstract class provides, are getting all the services they need. This method means that we are essentially wanting to do a “black box” test on the abstract class and make sure that, any derived class that’s going to use it, will have what we want it to have.

So this shifts our focus a bit. How do we test that any “client” to the base class is receiving the services it needs? This could be quite a complex problem to test, because another important concept here is that, we want to test only one thing at a time. If we test derived classes of the base class (which might be a design problem in itself), we are essentially testing the functionality of the base classes as well, not to mention we’ll have to build those classes for our tests to work at all!

One of the most elegant solutions to this problem is actually very simple in concept – we’ll build objects just for our tests!

Mock objects

Mock objects are a very handy technique to tests objects that are “mediators”. Instead of referring to a real domain object, we call the mock object which pretends to be the real object. The mock object is used to validate any assertions and expectations we have of that object and we can fully interact with it, while at the same time still have total control over the results of our object’s method calls. There’s a whole Mock Objects testing framework, which is an alternate view of unit testing.

You can learn more about mock objects by starting from this link.

Mock objects come in very handy when we either want to test an object that “changes stuff” in our application and we want to stop it, control it, or when we want to test an object that uses other objects in order to do its work. A data layer object that uses the database would be a good example of when we want to control such things (we never want our unit tests to corrupt live data, that’s rule #1, ask your DBA).

In this case, we can use a mock object to derive from our class and make sure that it, as a “client” to our class, receives all the necessary “services” it expects.

A simple project idea

In order to explain this in more “close to home” terms, I’ll make up a simple project task.

We’ve designed a simple object model in which we have an abstract class Task which will contain a start method. The class will be used to derive other tasks from it, but will provide each derived class with abstract methods that will need to be implemented: BeforeStart(), OnStart(), and AfterStart(). Each derived class will be able to use these methods to perform initialization before a task begins, the task itself when needed, and cleanup after the task has finished (think ServicedComponent type events).

Planning our first test

Ok. We have no code yet, but what do we test first? We want to make sure that for any derived class, calling the Start() method, actually triggers the BeforeStart(), OnStart(), and AfterStart() methods inside it. To do that, we’ll need an instance of a class derived from Task (which does not exist yet).

This is a perfect candidate for a mock object. We can use a mock object that will derive from our base class and will let us know if its inner methods were called.

We’ll want to used a very simple mechanism here.

Our first test

In our first test, we’ll assume we have an object derived from Task and we'll call it’s Start() method. Then we’ll assert that all three inert methods were called.

[Test]
public void TestOnStartCalled()
{
  MockTask task = new MockTask();
  task.Start();
  Assert.IsTrue(task.OnStartCalled);
  Assert.IsTrue(task.BeforeStartCalled);
  Assert.IsTrue(task.AfterStartCalled);
}

This code won’t compile because we don’t have any MockTask class defined.

Creating our mock object

Our mock object will derive from Task. Notice we’re building it assuming Task already exists.

public class MockTask:Task
{
  public bool OnStartCalled=false;
  public bool BeforeStartCalled=false;
  public bool AfterStartCalled=false;
}

The beauty of this is, because this is our mock object, we can make it do whatever we want. In this case we are simply adding flags to it. Those flags will have to change somehow later on, but that’s not our problem now. Now we’re concerned about making the code compile.

Our code still won’t compile because we haven’t created out Task class yet.

Creating our abstract class

public abstract class Task
{
  public void Start()
  {
 
  }
}

Notice we’re making the class as simple as possible, just making our code compile.

Making the test fail

If we run the test now out, code will compile, but the test will fail miserably. The derived class had no OnStart, BeforeStart and AfterStart methods defined. Therefore it’s called flags will always remain false.

Making the test work

Let’s make the test work by adding the required functionality to our base class:

public abstract class Task
{
  public void Start()
  {
     BeforeStart();
     OnStart();
     AfterStart();
  }

  //all base classes must implement this method
  protected abstract void OnStart();
  protected abstract void BeforeStart();
  protected abstract void AfterStart();
}

public class MockTask:Task
{
  public bool OnStartCalled=false;
  public bool BeforeStartCalled=false;
  public bool AfterStartCalled=false;
  
  protected override void AfterStart()
  {
    AfterStartCalled=true; 
  }
  protected override void BeforeStart()
  {
    BeforeStartCalled=true;
  }
  protected override void OnStart()
  {
    OnStartCalled=true;
  }
}

Conclusion

“Be-a-utiful!” as Bruce almighty often remarks. We now have a repeatable test that makes sure our base class calls all the methods we require from it.

References

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Roy Osherove
Web Developer
Israel Israel
No Biography provided

Comments and Discussions

 
Generalunit testing Pinmember35647815-Jun-05 18:36 
GeneralRe: unit testing PinmemberChristian Graus15-Jun-05 19:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140827.1 | Last Updated 24 Sep 2003
Article Copyright 2003 by Roy Osherove
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid