Mock objects in Java unit tests (deel 2)

In het eerste deel van dit blog artikel heb ik het gehad over hoe mock objecten zijn te maken met EasyMock, jmockit, en hoe er zelf een framework voor te maken is. In dit tweede deel laat ik wat zien van hoe deze frameworks technisch zijn geimplementeerd.

(zie het vorige deel van dit blog artikel voor een inleiding op het onderwerp)

Hoe zijn de mocking frameworks geimplementeerd?

java.lang.reflect.Proxy

Zoals de vorige keer al gezegd, voor mij was de directe aanleiding voor deze artikelen om zelf een framework(je) te bouwen (download). Leuke reden om weer eens met java.lang.reflect.Proxy te doen.

Hier is een redelijk volledige implementatie voor de gebruikte methode in het derde test voorbeeld van deel 1 van dit artikel:

 public static <T> T createMock(Class<T> classToMock, final Object mockObject) { 
   InvocationHandler handler = new InvocationHandler() { 
     @Override 
     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
       try { 
         Method mockMethod = mockObject.getClass().
             getMethod(method.getName(),
             method.getParameterTypes()); 
         mockMethod.setAccessible(true); 
         return mockMethod.invoke(mockObject, args); 
       } catch (NoSuchMethodException nsme) { 
         // Dit is het grootste issue met deze simpele 
         // implementatie, null is niet toegestaan als 
         // de methode primitive waarden opleveren.
         return null; 
       } 
     } 
   }; 
   T proxy = (T) Proxy.newProxyInstance(
       classToMock.getClassLoader(),
       new Class[] { classToMock },
       handler); 
   return proxy; 
 }

Al het echt lastige werk wordt door Java’s Proxy en InvocationHandler gedaan, die zorgen voor het aanleveren van een object dat een gegeven interface implementeert, en dat bij methode aanroepen op dat object ingegrepen kan worden in de invoke methode.

cglib

Java’s Proxy klasse kan alleen gebruikt worden om interfaces te mocken. EasyMock kan met een extensie naast interfaces ook klassen mocken. In de implementatie wordt het lastige werk gedaan door de cglib library. Hier een gewijzigd voorbeeld van hoe cglib te gebruiken is om een proxy voor een klasse te maken (de invocation handler blijft hetzelfde):

import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
final InvocationHandler invocationHandler = 
    new SimpleMockInvocationHandler(mockObject, behavior, wrappedObject, mockOptions);
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(classToMock);
Class mockClass = enhancer.createClass();
Enhancer.registerCallbacks(mockClass, new Callback[] {
    new MethodInterceptor() {
      public Object intercept(Object obj, Method method,
          Object[] args, MethodProxy arg3)
          throws Throwable {
        return invocationHandler.invoke(obj, method, args);
      }
    }
});
// Een goede constructor selecteren is in de praktijk wat lastiger
mockClass.getConstructors()[0].newInstance(new Object[0]);

Voor SimpleMock heb ik hetzelfde toegepast waardoor klassen gemockt kunnen worden, hoewel het nu eigenlijk alleen werkt voor klassen met een lege constructor.

Java instrumentation en ASM

jmockit werkt in feite op een heel andere manier. Via Java instrumentation (beschikbaar sinds Java 5) wordt het mogelijk gemaakt om klasse definities te overschrijven in de Java runtime. Bij instrumentation lever je een klasse met onder andere een static methode premain, om een idee te geven bijvoorbeeld:

// Te gebruiken door een jar te maken met Premain-Class attribuut in het manifest
public class MyInstrumentationClass {
  public static void premain(String agentArgs, Instrumentation instrumentation)
      throws Exception {
    byte[] classByteCodes = ...
    instrumentation.redefineClasses(
        new ClassDefinition(Date.class, classByteCodes));
    // andere leuke methoden zijn:
    // - instrumentation.appendToBootstrapClassLoaderSearch(jarfile);
    // - instrumentation.getObjectSize(objectToSize);
  }
}

De jar met deze klasse moet dan worden opgenomen als een speciaal JVM argument (bijvoorbeeld -javaagent:/pad/naar/jmockit.jar) waarna deze code wordt uitgevoerd voor elke andere code (het is dus ook handig als je in een JVM andere dingen wil doen voordat het echte programma start, zonder dat een andere klasse met een main methode hoeft te worden aangeroepen).

Voor het genereren van de byte code gebruikt jmockit ASM om een bestaande klassedefinitie in te laden en te manipuleren. Het genereren van de gewijzigde instructies gaat dan met code als:

MethodVisitor methodVisitor = null;
methodVisitor.visitIntInsn(SIPUSH, mockInstanceIndex);
methodVisitor.visitMethodInsn(INVOKESTATIC,
    "mockit/internal/MockFixture", "getMock", 
    "(I)Ljava/lang/Object;");
methodVisitor.visitTypeInsn(CHECKCAST, mockClassName);

(dit is de generatie van 1 simpele methode aanroep).

Je moet dus in feite zelf de Java bytecode genereren. Dat vind ik zelf dus niet eenvoudig meer te noemen, en in mijn eigen framework(je) ben ik hier dus ook zeker niet aan begonnen.


Reageer

RSS feed for comments on this post · TrackBack URI