2015-06-30 4 views
5

W ramach testowania wtyczki Gradle chciałbym wykręcić ciekawą metodę: project.exec {...}. To jest potwierdzenie, że wykonuje poprawne wywołania linii poleceń. I próbował to przy użyciu metaprogramowanie:Mock Gradle project.exec {...} przy użyciu metaClass

Project proj = ProjectBuilder.builder().build() 

proj.metaClass.exec = { Closure obj -> 
    println 'MOCK EXEC' 
} 

proj.exec { 
    executable 'echo' 
    args 'PROJECT EXEC' 
} 
// prints 'PROJECT EXEC' instead of the 'MOCK EXEC' I expected 

Co ciekawe jest, że jeśli zmiana nazwy obu exec metod do othername, to działa prawidłowo:

Project proj = ProjectBuilder.builder().build() 

proj.metaClass.othername = { Closure obj -> 
    println 'MOCK EXEC' 
} 

proj.othername { 
    executable 'echo' 
    args 'PROJECT EXEC' 
} 
// prints 'MOCK EXEC' as expected 

próbuję dowiedzieć się, dlaczego istniejąca project.exec Metoda powoduje niepowodzenie metaprogramingu i istnieje obejście tego problemu. Zauważ, że Project jest interfejsem, ale ja kpię z konkretnej instancji typu DefaultProject.

Sposób metaprogramming do stubbing się do jednej metody z tej odpowiedzi: https://stackoverflow.com/a/23818476/1509221

Odpowiedz

1

w Groovy zastąpienia sposobu zdefiniowanego w interfejsie stosując metaklasą jest uszkodzony. W tym przypadku metoda exec jest zdefiniowana w klasie Project, która jest interfejsem. Od GROOVY-3493 (zgłoszone pierwotnie w 2009 roku):

"Cannot override methods via metaclass that are part of an interface implementation" 

Obejście problemu

invokeMethod przechwytuje wszystkie metody i może pracować. To jest przesada, ale działa. Gdy nazwa metody pasuje do exec, przekazuje połączenie do obiektu mySpecialInstance. W przeciwnym razie jest przekazywana do delegata, a mianowicie do istniejących metod. Dzięki invokeMethod delegation i Logging All Methods do wprowadzania na tym.

// This intercepts all methods, stubbing out exec and passing through all other invokes 
this.project.metaClass.invokeMethod = { String name, args -> 
    if (name == 'exec') { 
     // Call special instance to track verifications 
     mySpecialInstance.exec((Closure) args.first()) 
    } else { 
     // This calls the delegate without causing infinite recursion 
     MetaMethod metaMethod = delegate.class.metaClass.getMetaMethod(name, args) 
     return metaMethod?.invoke(delegate, args) 
    } 
} 

Działa to dobrze, z wyjątkiem, że może pojawić się wyjątki o „złym liczbą argumentów” lub „nie może powoływać się na metody xxxxx pustego obiektu”. Problem polega na tym, że powyższy kod nie radzi sobie z wymuszaniem argumentów metody. W przypadku parametru project.files(Object... paths) argumenty dla metody invokeMethod powinny mieć postać [['path1', 'path2']]. ALE, w niektórych przypadkach jest wywołanie files(null) lub files(), więc argumenty dla invokeMethod okazują się odpowiednio [null] i [], które nie spełniają oczekiwań, ponieważ oczekują one [[]]. Wyprodukowanie wyżej wymienionych błędów.

Następujący kod rozwiązuje to tylko dla metody files, ale to wystarczyło dla moich testów jednostkowych. Nadal chciałbym znaleźć lepszy sposób na wymuszanie typów lub najlepiej zastąpienie jednej metody.

// As above but handle coercing of the files parameter types 
this.project.metaClass.invokeMethod = { String name, args -> 
    if (name == 'exec') { 
     // Call special instance to track verifications 
     mySpecialInstance.exec((Closure) args.first()) 
    } else { 
     // This calls the delegate without causing infinite recursion 
     // https://stackoverflow.com/a/10126006/1509221 
     MetaMethod metaMethod = delegate.class.metaClass.getMetaMethod(name, args) 
     logInvokeMethod(name, args, metaMethod) 

     // Special case 'files' method which can throw exceptions 
     if (name == 'files') { 
      // Coerce the arguments to match the signature of Project.files(Object... paths) 
      // TODO: is there a way to do this automatically, e.g. coerceArgumentsToClasses? 
      assert 0 == args.size() || 1 == args.size() 

      if (args.size() == 0 || // files() 
       args.first() == null) { // files(null) 
       return metaMethod?.invoke(delegate, [[] as Object[]] as Object[]) 
      } else { 
       // files(ArrayList) possibly, so cast ArrayList to Object[] 
       return metaMethod?.invoke(delegate, [(Object[]) args.first()] as Object[]) 
      } 
     } else { 
      // Normal pass through 
      return metaMethod?.invoke(delegate, args) 
     } 
    } 
} 
+0

Nie jestem w 100% pewny, że zadziała, ale możesz użyć tej techniki w innym poście na moim blogu na temat nadpisywania metod, ale wciąż używając oryginalnej implementacji: http://naleid.com/blog/2009/06/01/groovy-metaclass-overriding-a-method-while-using-the-old-implementation To może zostać zablokowane przez problem interfejsu, który zidentyfikowałeś chociaż ... Jeśli tak, to rozwiązanie, które dla mnie wygląda jak dobre. –

+0

Dzięki Ted, ten sam problem z interfejsem uniemożliwia jego działanie. Interesują mnie twoje przemyślenia na temat typów wymuszających. Prosty kod kończy się niepowodzeniem dla 'project.files (Object ... paths)' dla 'files (null)' i 'files()'. Czy istnieje lepszy sposób wymuszania typów parametru varargs w sposób ogólny? Zapraszam do edytowania mojego posta z ulepszeniami. – brunobowden