2008-09-14 17 views
35

Mein Motto für Java ist "nur weil Java statische Blöcke hat, bedeutet es nicht, dass Sie sie verwenden sollten." Witze beiseite, es gibt eine Menge Tricks in Java, die das Testen zu einem Albtraum machen. Zwei der meisten, die ich hasse, sind anonyme Klassen und statische Blöcke. Wir haben eine Menge Legacy-Code, der statische Blöcke verwendet, und diese sind einer der ärgerlichen Punkte in unseren Push-in-Unit-Tests. Unser Ziel ist es, Komponententests für Klassen schreiben zu können, die von dieser statischen Initialisierung mit minimalen Codeänderungen abhängig sind.Mocking statische Blöcke in Java

Bis jetzt ist mein Vorschlag zu meinen Kollegen, den Körper des statischen Blockes in eine private statische Methode zu bewegen und es staticInit zu nennen. Diese Methode kann dann innerhalb des statischen Blocks aufgerufen werden. Beim Komponententest kann eine andere Klasse, die von dieser Klasse abhängt, leicht staticInit mit JMockit überspielen, um nichts zu tun. Lass uns das im Beispiel sehen.

public class ClassWithStaticInit { 
    static { 
    System.out.println("static initializer."); 
    } 
} 

Wird

public class ClassWithStaticInit { 
    static { 
    staticInit(); 
    } 

    private static void staticInit() { 
    System.out.println("static initialized."); 
    } 
} 

Damit wir in einem JUnit folgendes tun können, um

geändert werden.

public class DependentClassTest { 
    public static class MockClassWithStaticInit { 
    public static void staticInit() { 
    } 
    } 

    @BeforeClass 
    public static void setUpBeforeClass() { 
    Mockit.redefineMethods(ClassWithStaticInit.class, MockClassWithStaticInit.class); 
    } 
} 

Allerdings hat diese Lösung auch ihre eigenen Probleme. Sie können DependentClassTest und ClassWithStaticInitTest nicht auf derselben JVM ausführen, da Sie den statischen Block tatsächlich für ClassWithStaticInitTest ausführen möchten.

Was wäre Ihre Art, diese Aufgabe zu erfüllen? Oder irgendwelche besseren, nicht auf JMockit basierenden Lösungen, von denen Sie denken, dass sie sauberer arbeiten würden?

Antwort

5

Als ich in dieses Problem, ich in der Regel die gleiche Sache, die Sie beschreiben, außer ich die statische Methode machen geschützt, so kann ich es manuell aufrufen. Obendrein stelle ich sicher, dass die Methode mehrmals ohne Probleme aufgerufen werden kann (sonst ist sie nicht besser als der statische Initialisierer, was die Tests betrifft).

Dies funktioniert ziemlich gut, und ich kann tatsächlich testen, dass die statische Initialisierungsmethode tut, was ich erwarte/will. Manchmal ist es am einfachsten, einen statischen Initialisierungscode zu haben, und es lohnt sich einfach nicht, ein übermäßig komplexes System zu erstellen, um es zu ersetzen.

Wenn ich diesen Mechanismus verwende, stelle ich sicher, dass die geschützte Methode nur zu Testzwecken verfügbar ist, mit der Hoffnung, dass sie nicht von anderen Entwicklern verwendet wird. Dies kann natürlich keine brauchbare Lösung sein, beispielsweise wenn die Schnittstelle der Klasse extern sichtbar ist (entweder als eine Unterkomponente irgendeiner Art für andere Teams oder als ein öffentlicher Rahmen). Es ist eine einfache Lösung für das Problem, und erfordert keine Drittanbieter-Bibliothek einzurichten (was ich mag).

4

Klingt für mich wie Sie ein Symptom behandeln: schlechtes Design mit Abhängigkeiten von der statischen Initialisierung. Vielleicht ist ein Refactoring die wirkliche Lösung. Es klingt, als ob Sie bereits etwas mit Ihrer staticInit() Funktion refaktoriert haben, aber vielleicht muss diese Funktion vom Konstruktor aufgerufen werden, nicht von einem statischen Initialisierer. Wenn Sie die Zeit für statische Initialisierer abschaffen können, sind Sie besser dran. Nur Sie können diese Entscheidung treffen (Ich kann Ihre Codebasis nicht sehen), aber einige Refactoring wird definitiv helfen.

Wie zum Spotten, benutze ich EasyMock, aber ich habe in das gleiche Problem geraten. Nebeneffekte von statischen Initialisierern im Legacy-Code erschweren das Testen. Unsere Antwort war, den statischen Initialisierer umzuformen.

+0

Sie können mit EasyMock nicht "statische" oder "private" Methoden vortäuschen. Das Bewegen des Körpers des statischen Initialisierers ist so weit von einem Refactoring, das wir für jetzt tun können. –

+0

Wenn die zu testende Klasse die statische Methode init/private hat, soll sie aufgerufen werden. Kein Problem. Aber wenn es die Klasse ist, die verspottet wird, ist das kein Problem für leichtes Spott: Es wird nicht aufgerufen, weil es KEINE UMSETZUNG gibt. Sie können sich die öffentlichen Schnittstellen so anhören, als wären die privaten Daten nicht vorhanden. –

1

Ich nehme an, Sie wollen wirklich eine Art Fabrik anstelle des statischen Initialisierers.

Eine Mischung aus einem Singleton und einer abstrakten Fabrik würde wahrscheinlich in der Lage sein, die gleiche Funktionalität wie heute zu bekommen, und mit guter Testbarkeit, aber das würde ziemlich viel Kode hinzufügen, also könnte es besser sein Versuchen Sie einfach, das statische Material vollständig zu refaktorisieren oder wenn Sie zumindest mit einer weniger komplexen Lösung davonkommen könnten.

Schwer zu sagen, ob es möglich ist, ohne Ihren Code zu sehen.

3

Sie könnten Ihren Testcode in Groovy schreiben und die statische Methode mit Metaprogrammierung leicht nachahmen.

Math.metaClass.'static'.max = { int a, int b -> 
    a + b 
} 

Math.max 1, 2 

Wenn Sie nicht Groovy verwenden, werden Sie wirklich brauchen, um den Code zu Refactoring (vielleicht so etwas wie ein initializator zu injizieren).

Mit freundlichen Grüßen

+0

Ich liebe Groovy, aber leider muss unser Testcode in JUnit sein. –

+0

Cem, Aber Sie könnten Junit (sogar Junit4) Tests in groovy schreiben. ;-) Mit freundlichen Grüßen – marcospereira

1

Ich bin nicht sehr gut informiert in Mock Frameworks also bitte korrigieren Sie mich, wenn ich falsch liege, aber könnten Sie möglicherweise zwei verschiedene Mock-Objekte, um die Situationen, die Sie erwähnen, zu decken? Wie

public static class MockClassWithEmptyStaticInit { 
    public static void staticInit() { 
    } 
} 

und

public static class MockClassWithStaticInit { 
    public static void staticInit() { 
    System.out.println("static initialized."); 
    } 
} 

Dann können Sie sie in Ihren verschiedenen Testfälle

@BeforeClass 
public static void setUpBeforeClass() { 
    Mockit.redefineMethods(ClassWithStaticInit.class, 
         MockClassWithEmptyStaticInit.class); 
} 

und

@BeforeClass 
public static void setUpBeforeClass() { 
    Mockit.redefineMethods(ClassWithStaticInit.class, 
         MockClassWithStaticInit.class); 
} 

jeweils verwenden.

+0

'System.out.println (" statisch initialisiert. ");' Ist das, was der statische Initialisierer an erster Stelle macht. Warum sollten wir uns darüber lustig machen, was es bereits mit der gleichen Sache macht? –

+0

Cem, ich glaube, du hast meinen Punkt verpasst. Ich versuche, das Problem zu lösen "Diese Lösung hat jedoch auch ihre eigenen Probleme. Sie können DependentClassTest und ClassWithStaticInitTest nicht auf derselben JVM ausführen, da Sie eigentlich möchten, dass der statische Block für ClassWithStaticInitTest ausgeführt wird." – martinatime

+0

Grundsätzlich haben Sie zwei verschiedene Mock-Objekte zu Mock ClassWithStaticInit eine für die Verwendung mit Ihrem DependentClassTest und die andere für die Verwendung mit ClassWithStaticInitTest. – martinatime

10

Dies wird in mehr "Advanced" JMockit bekommen. Es stellt sich heraus, dass Sie statische Initialisierungsblöcke in JMockit neu definieren können, indem Sie eine Methode public void $clinit() erstellen. Anstatt also diese Änderung machen

public class ClassWithStaticInit { 
    static { 
    staticInit(); 
    } 

    private static void staticInit() { 
    System.out.println("static initialized."); 
    } 
} 

können wir auch ClassWithStaticInit lassen wie es ist und gehen Sie wie folgt in der MockClassWithStaticInit:

public static class MockClassWithStaticInit { 
    public void $clinit() { 
    } 
} 

Dies wird in der Tat erlauben es uns nicht, um Änderungen in der machen vorhandene Klassen.

+0

Dies sollte die akzeptierte Antwort sein. Als eine Ergänzung; Der öffentliche Accessor wird nicht für $ clinit mit einer @Mock-Annotation benötigt. – user2219808

32

PowerMock ist ein weiteres Mock-Framework, das EasyMock und Mockito erweitert. Mit PowerMock können Sie ganz einfach remove unwanted behavior aus einer Klasse, zum Beispiel einem statischen Initialisierer. In Ihrem Beispiel fügen Sie einfach die folgenden Anmerkungen zu Ihrem JUnit-Testfall:

@RunWith(PowerMockRunner.class) 
@SuppressStaticInitializationFor("some.package.ClassWithStaticInit") 

PowerMock verwendet keine Java-Agenten und erfordert daher keine Änderung der Startparameter JVM. Sie fügen einfach die JAR-Datei und die obigen Anmerkungen hinzu.

9

Gelegentlich finde ich statische Initialisierer in Klassen, von denen mein Code abhängt.Wenn ich den Code nicht Refactoring kann, verwende ich PowerMock ‚s @SuppressStaticInitializationFor Annotation die statischen Initialisierer zu unterdrücken:

@RunWith(PowerMockRunner.class) 
@SuppressStaticInitializationFor("com.example.ClassWithStaticInit") 
public class ClassWithStaticInitTest { 

    ClassWithStaticInit tested; 

    @Before 
    public void setUp() { 
     tested = new ClassWithStaticInit(); 
    } 

    @Test 
    public void testSuppressStaticInitializer() { 
     asserNotNull(tested); 
    } 

    // more tests... 
} 

Lesen Sie mehr über suppressing unwanted behaviour.

Haftungsausschluss: PowerMock ist ein Open-Source-Projekt, das von zwei meiner Kollegen entwickelt wurde.

0

Nicht wirklich eine Antwort, aber nur wundernd - gibt es keine Möglichkeit, den Anruf zu Mockit.redefineMethods "umzukehren"?
Wenn eine solche explizite Methode nicht existiert, sollte sie nicht wie folgt ausgeführt werden?

Mockit.redefineMethods(ClassWithStaticInit.class, ClassWithStaticInit.class); 

Wenn ein solches Verfahren existiert, können Sie es in der Klasse ausführen @AfterClass Verfahren und Test ClassWithStaticInitTest mit dem ‚Original‘ statischen Initialisierungsblocks, als ob sich nichts geändert hat, von der gleichen JVM.

Dies ist nur eine Ahnung, so kann ich etwas verpassen.