Proxy, Decorator, Adapter, Façade, ...
How to Test a Wrapper?
Context
A wrapper contains another instance, the delegate, to which it transfers any call it receives. The calls of the user to a wrapper are transferred to the delegate, and the answers of the delegate are transferred back by the wrapper to the user. The whole point of a wrapper is, in other words, to play as an intermediary between the user and the delegate.
Different types of wrappers can be designed:
-
A proxy contains a delegate which implements the same interface, delegate which can be changed on demand.
The proxy provides a single instance to access a dynamic instance (which changes over time).
It may be used to delay the instanciation of the delegate.
interface Foo { void doSomething(); }
class FooProxy implements Foo { private Foo delegate; void setDelegate(Foo newDelegate) { this.delegate = newDelegate; } public void doSomething() { delegate.doSomething(); } }
-
A decorator provides additional logics to its delegate.
It can add pre-processing and post-processing, like checks, logs, etc.
interface Foo { void doSomething(); }
class FooDecorator implements Foo { private final Foo delegate; public FooDecorator(Foo foo) { this.delegate = foo; } public void doSomething() { doSomethingBefore(); delegate.doSomething(); doSomethingAfter(); } private void doSomethingBefore() {...} private void doSomethingAfter() {...} }
-
An adapter translates a source interface (or class) into a target interface.
It allows to use a source instance like a target instance, and it may add some translation logics to adapt the parameters and the returned value.
interface Source { long doSomething(char[] sourceArg); }
interface Target { int doItAnotherWay(String targetArg); }
class SourceTargetAdapter implements Target { private final Source delegate; SourceTargetAdapter(Source source) { this.delegate = source; } public int doItAnotherWay(String targetArg) { char[] sourceArg = targetArg.toCharArray(); // Param translation long sourceResult = delegate.doSomething(sourceArg); int targetResult = (int) sourceResult; // Result translation return targetResult; } }
-
A façade aims for simplifying interfaces.
It may remove parameters because they always get the same value, or remove unused methods, or bring several related interfaces into a single, complete one.
interface Foo { void foo1(String arg1, int arg2, boolean arg3); void foo2(char[] arg1, boolean arg2); }
interface Bar { void bar(); }
class FooMethodReductionFacade { private final Foo foo; public FooMethodReductionFacade(Foo foo) { this.foo = foo; } public void foo1(String arg1, int arg2, boolean arg3) { foo.foo1(arg1, arg2, arg3); } // No foo2 method because not wanted }
class FooParamReductionFacade { private final Foo foo; private final int intArg = 0; private final boolean boolArg = true; public FooParamReductionFacade(Foo foo) { this.foo = foo; } public void foo1(String arg) { // No param 2 & 3 foo.foo1(arg, intArg, boolArg); // Use defaults } public void foo2(char[] arg) { // No param 2 foo.foo2(arg, boolArg); // Use default } }
class FooBarMergingFacade implements Foo, Bar { private final Foo foo; private final Bar bar; public FooBarMergingFacade(Foo foo, Bar bar) { this.foo = foo; this.bar = bar; } // Expose Foo method public void foo1(String arg, int arg2, boolean arg3) { foo.foo1(arg, arg2, arg3); } // Expose Foo method public void foo2(char[] arg, boolean arg2) { foo.foo2(arg, arg2); } // Expose Bar method public void bar() { bar.bar(); } }
Although the term of wrapper have been initially used only for the adapter and the decorator patterns, more specific terms exist to do the difference. Their common aspect, i.e. the delegate, becomes the main aspect of the more generic concept of wrapper. Since this aspect applies also to other designs, as shown above, we use the term of wrapper more broadly in this article. The examples above are not the only ones, and a single class can combine several of them depending on the needs. Generally speaking, any class that relies on a delegate to provide the main feature will be considered as a wrapper class in this article.
Question: How to Test it?
The wrapper, because it relies on its delegate to provide the main feature, should preserve its behaviour. But different delegates my come with different behaviours, so should we test the wrapper with any possible delegate? Such an objective would be unreasonable: there may have to much possibilites to properly test all the relevant behaviours.
Some of them implement a single interface, so we may at least test the common behaviours. But since the wrapper itself does not store the logic, it would need to be tested with a specific instance. Such a stratgy would also be unreasonable: we would confirm the wrapper only for a single delegate. Morover, the delegate may require a huge set up effort and, since it may be already tested alone, it seems unreasonable to spend all this effort again to test the wrapper.
Solution: Focus on Data Transfer
In fact, the solution is rather trivial and systematic. Indeed, the contract of a wrapper is not to provide the features of the delegate, but to wrap it. This delegate is the one holding the contract of the feature to implement. The only contract of the wrapper is then to properly play its intermediary role. In other words, it should be tested on its ability to transfer the calls to the delegate, and transfer back the answer of this delegate.
Let's take this interface as an example:
interface Foo {
boolean doSomething(String arg1, int arg2);
}
And this wrapper implementation:
class FooWrapper {
private final Foo delegate;
private final int hiddenArg = 0;
public FooWrapper(Foo foo) {
this.delegate = foo;
}
public boolean doSomething(String arg) {
return delegate.doSomething(arg, hiddenArg);
}
}
A proper test would check that, for each method:
- the parameters are correctly transferred (including translation & hidden parameters) to the delegate method ;
- the result is correctly transferred (including translation) from the delegate method ;
- if no parameter nor result should be transferred, the delegate method is correctly called.
public class FooWrapperTest {
@Test
public void testDoSomethingIsCorrectlyMapped() {
// Decide the expected values
String expectedShownArg = "test";
int expectedHiddenArg = 0;
boolean expectedResult = true;
// Store the transferred values
String[] actualShownArg = {null};
Integer[] actualHiddenArg = {null};
FooWrapper wrapper = new FooWrapper(new Foo() {
public boolean doSomething(String arg1, int arg2) {
actualShownArg[0] = arg1;
actualHiddenArg[0] = arg2;
return expectedResult;
}
});
boolean actualResult = wrapper.doSomething(expectedShownArg);
// Check they correspond
assertEquals(expectedShownArg, actualShownArg[0]);
assertEquals(expectedHiddenArg, actualHiddenArg[0], 0);
assertEquals(expectedResult, actualResult);
}
}
The key point is to check that everything is properly transferred. Be careful if you prefer to write the arguments assertions in the delegate method. If it is not called, and you have no returned value to check, you may have a passing test without calling the delegate:
class FooWrapper {
private final Foo delegate;
private final int hiddenArg = 0;
public FooWrapper(Foo foo) {
this.delegate = foo;
}
public void doSomething(String arg) {
// Forget to call the delegate
}
}
public class FooWrapperTest {
@Test
public void testDoSomethingIsCorrectlyMapped() {
String expectedShownArg = "test";
int expectedHiddenArg = 0;
FooWrapper wrapper = new FooWrapper(new Foo() {
public void doSomething(String arg1, int arg2) {
// Never called, so no assertion fail and all is green
assertEquals(expectedShownArg, arg1);
assertEquals(expectedHiddenArg, arg2, 0);
}
});
wrapper.doSomething(expectedShownArg);
}
}
Prefer to systematically check the method is called in this case:
public class FooWrapperTest {
@Test
public void testDoSomethingIsCorrectlyMapped() {
String expectedShownArg = "test";
int expectedHiddenArg = 0;
boolean[] isCalled = {false};
FooWrapper wrapper = new FooWrapper(new Foo() {
public void doSomething(String arg1, int arg2) {
assertEquals(expectedShownArg, arg1);
assertEquals(expectedHiddenArg, arg2, 0);
isCalled[0] = true;
}
});
wrapper.doSomething(expectedShownArg);
assertTrue(isCalled[0]); // Fail here
}
}