6.13 External Components


6.13 External Components

Using dummy objects for testing works wonderfully as long as our helper or server objects allow the simple creation of dummies. Working exclusively with our own code, we can produce this type of testability, at worst, by major refactoring actions. If we proceed entirely by the test-first approach, then testability will just turn out that way, without making a major effort.

The situation is different for our interfaces to the Java library or third-party components. The external APIs we use are encapsulated by means of interfaces or abstract classes and can be replaced by mock objects in our tests only if we are extremely lucky. One good example for this is the class java.io.OutputStream; remember how easy it was for us to replace it by our MockTextOutputStream. In contrast, when working with third-party libraries or dealing with parts of the Java library left over from JDK 1.0, the situation normally looks like this: [5]

 import thirdparty.*; public class MyClient {       public void doSomething(String arg) {          TheirRequest request = new TheirRequest(arg);          TheirResponse response = request.send();          String answer = response.getAnswer();          // do something with answer...       } } 

In this example, the Their* classes are the interfaces provided by the third-party vendor; MyClient is our own class. Based on what we have learned so far, we now plan the following approach: We build a class, Mock-Request, derived from TheirRequest. The latter will then return an instance of MockResponse upon send(), where MockResponse is derived from Their-Response. Of course, we have previously defined the latter's response to getAnswer(). If we manage to realize this plan, we will also have managed to tame this external interface. However, such an attempt often fails due to one or several of the following reasons:

  • TheirRequest and/or TheirResponse are final and no subclasses can be derived from them.

  • The classes themselves are not final, but the methods send() and/or getAnswer() are.

  • The existing constructors of TheirResponse are not usable for a mock subclass, because they need parameter objects, which cannot be created outside the library, or we need new parameters to create them and so on.

  • The library classes do things that we wanted to avoid with our mock approach, such as accessing a network.

Complaining about the shortcomings of unknown developers won't help either. Nope, we have to solve our test problem ourselves. With the source code on hand, we could modify the corresponding classes so they will no longer conflict with our testing efforts. However, this could represent a millstone around our necks with regard to future versions of that external library.

Isn't there another way? There is. Let's simply build another layer around that external interface. First, we define an interface that defines the functionality of the external library for our special needs. This approach is described as the adapter pattern in Design Patterns [Gamma95]. In our current example, things would look like this:

 public interface AnswerFactory {    String getAnswer(String arg); } 

From now on, our own client uses only this "factory" to create an answer object:

 import thirdparty.*; public class MyClient {    private AnswerFactory factory;    public MyClient(AnswerFactory factory) {       this.factory = factory;    }    public void doSomething(String arg) {       String answer = factory.getAnswer(arg);       // do something with answer...    } } 

What's missing now are only the two implementations of Answer-Factory:

 public class MockAnswerFactory implements AnswerFactory {    private String answer;    public MockAnswerFactory(String presetAnswer) {       answer = presetAnswer;    }    public String getAnswer(String arg) {       return answer;    } } import thirdparty.*; public class AnswerFactoryAdaptor implements AnswerFactory {    public String getAnswer(String arg) {       TheirRequest request = new TheirRequest(arg);       TheirResponse response = request.send();       return response.getAnswer();    } } 

And here we are, exactly where we wanted to be: we now have a mock object we can use to test our MyClient class. But wait, there is now a gap in the whole thing: the class AnswerFactoryAdaptor is left untested. If we were able to test it, we could have saved ourselves a lot of trouble. For this reason, the methods of this "forwarding class" should remain as simple as possible. If we need more logic in this class, in addition to the sheer translation of method calls, then another separation into adapter and delegator is recommended.

By the way, we won something else in addition to testability: our client is now independent of the external interface. Just in case we decide to use another library later on, we would only have to replace the Adaptor class.

It is normally not our job to test the functionality of an external library; this should have been done elsewhere. Still, it appears meaningful to add a handful of tests covering our special use of a library to ensure that we understand the interface and that a new version of that library would function properly. But that's another story.

[5]This example was adapted from the discussion at [URL:WikiUTATP].




Unit Testing in Java. How Tests Drive the Code
Unit Testing in Java: How Tests Drive the Code (The Morgan Kaufmann Series in Software Engineering and Programming)
ISBN: 1558608680
EAN: 2147483647
Year: 2003
Pages: 144
Authors: Johannes Link

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net