Monday, February 24, 2014

How to Think About the "new" Operator with Respect to Unit Testing

By Miško Hevery

  • Unit Testing as the name implies asks you to test a Class (Unit) in isolation.
  • If your code mixes Object Construction with Logic you will never be able to achieve isolation.
  • In order to unit-test you need to separate object graph construction from the application logic into two different classes
  • The end goal is to have either: classes with logic OR classes with “new” operators.

Unit-Testing as the name implies is testing of a Unit (most likely a Class) in isolation. Suppose you have a class House. In your JUnit test-case you simply instantiate the class House, set it to a particular state, call the method you want to test and then assert that the class’ final state is what you would expect. Simple stuff really…

class House {
  private boolean isLocked;

  private boolean isLocked() {
    return isLocked;
  }

  private boolean lock() {
    isLocked = true;
  }
}

If you look at House closely you will realize that this class is a leaf of your application. By leaf I mean that it is the end of the dependency graph, it does not reference any other classes. As such all leafs of any code base are easy to tests, because the dependency graph ends with the leaf. But testing classes which are not leafs can be a problem because we may not be able to instantiate the class in isolation.

class House {
  private final Kitchen kitchen = new Kitchen();
  private boolean isLocked;

  private boolean isLocked() {
    return isLocked;
  }

  private boolean lock() {
    kitchen.lock();
    isLocked = true;
  }
}

In this updated version of House it is not possible to instantiate House without the Kitchen. The reason for this is that the new operator of Kitchen is embedded within the House logic and there is nothing we can do in a test to prevent the Kitchen from getting instantiated. We say that we are mixing the concern of application instantiation with concern of application logic. In order to achieve true unit testing we need to instantiate real House with a fake Kitchen so that we can unit-test the House in isolation.

class House {
  private final Kitchen kitchen;
  private boolean isLocked;

  public House(Kitchen kitchen) {
    this.kitchen = kitchen;
  }

  private boolean isLocked() {
  return isLocked;
}

  private boolean lock() {
    kitchen.lock();
    isLocked = true;
  }
}

Notice how we have removed the new operator from the application logic. This makes testing easy. To test we simply new-up a real House and use a mocking framework to create a fake Kitchen. This way we can still test House in isolation even if it is not a leaf of an application graph.

But where have the new operators gone? Well, we need a factory object which is responsible for instantiating the whole object graph of the application. An example of what such an object may look like is below. Notice how all of the new operators from your applications migrate here.

class ApplicationBuilder {
  House build() {
    return new House(new Kitchen(
      new Sink(),
      new Dishwasher(),
      new Refrigerator())
    );
  }
}

As a result your main method simply asks the ApplicationBuilder to construct the object graph for you application and then fires of the application by calling a method which does work.

class Main {
  public static void main(String...args) {
    House house = new ApplicationBuilder().build();
    house.lock();
  }
}

Asking for your dependencies instead of constructing them withing the application logic is called "Dependency Injection" and is nothing new in the unit-testing world. But the reason why Dependency Injection is so important is that within unit-tests you want to test a small subset of your application. The requirement is that you can construct that small subset of the application independently of the whole system. If you mix application logic with graph construction (the new operator) unit-testing becomes impossible for anything but the leaf nodes in your application. Without Dependency Injection the only kind of testing you can do is scenario-testing, where you instantiate the whole application and than pretend to be the user in some automated way.

Reference:
http://misko.hevery.com/2008/07/08/how-to-think-about-the-new-operator/

No comments: