Erweiterte JMockit-Verwendung

1. Einleitung

In diesem Artikel gehen wir über die JMockit-Grundlagen hinaus und betrachten einige erweiterte Szenarien, wie z.

  • Fälschung (oder die MockUp- API)
  • Die Deencapsulation- Dienstprogrammklasse
  • Wie man mehr als eine Schnittstelle mit nur einem Mock verspottet
  • So verwenden Sie Erwartungen und Überprüfungen wieder

Wenn Sie die Grundlagen von JMockit kennenlernen möchten, lesen Sie andere Artikel aus dieser Reihe. Sie finden relevante Links am Ende der Seite.

2. Maven-Abhängigkeit

Zuerst müssen wir die jmockit-Abhängigkeit zu unserem Projekt hinzufügen:

 org.jmockit jmockit 1.41  

Als nächstes fahren wir mit den Beispielen fort.

3. Verspotten privater Methoden / innerer Klassen

Das Verspotten und Testen privater Methoden oder innerer Klassen wird oft nicht als gute Praxis angesehen.

Der Grund dafür ist, dass sie, wenn sie privat sind, nicht direkt getestet werden sollten, da sie die innersten Eingeweide der Klasse sind, aber manchmal muss dies noch getan werden, insbesondere wenn es um Legacy-Code geht.

Mit JMockit haben Sie zwei Möglichkeiten, um diese zu behandeln:

  • Die MockUp- API zum Ändern der tatsächlichen Implementierung (für den zweiten Fall)
  • Die Deencapsulation- Dienstprogrammklasse, um eine beliebige Methode direkt aufzurufen (für den ersten Fall)

Alle folgenden Beispiele werden für die folgende Klasse erstellt, und wir nehmen an, dass sie in einer Testklasse mit derselben Konfiguration wie die erste ausgeführt werden (um zu vermeiden, dass sich Code wiederholt):

public class AdvancedCollaborator { int i; private int privateField = 5; // default constructor omitted public AdvancedCollaborator(String string) throws Exception{ i = string.length(); } public String methodThatCallsPrivateMethod(int i) { return privateMethod() + i; } public int methodThatReturnsThePrivateField() { return privateField; } private String privateMethod() { return "default:"; } class InnerAdvancedCollaborator {...} }

3.1. Fälschung mit MockUp

JMockit der Mockup - API bietet Unterstützung für die Schaffung von gefälschten Implementierungen oder Attrappen . In der Regel zielt ein Modell auf einige Methoden und / oder Konstruktoren in der Klasse ab, die gefälscht werden sollen, während die meisten anderen Methoden und Konstruktoren unverändert bleiben. Dies ermöglicht ein vollständiges Umschreiben einer Klasse, sodass jede Methode oder jeder Konstruktor (mit jedem Zugriffsmodifikator) als Ziel ausgewählt werden kann.

Mal sehen, wie wir privateMethod () mithilfe der Mockup-API neu definieren können :

@RunWith(JMockit.class) public class AdvancedCollaboratorTest { @Tested private AdvancedCollaborator mock; @Test public void testToMockUpPrivateMethod() { new MockUp() { @Mock private String privateMethod() { return "mocked: "; } }; String res = mock.methodThatCallsPrivateMethod(1); assertEquals("mocked: 1", res); } }

In diesem Beispiel definieren wir ein neues MockUp für die AdvancedCollaborator- Klasse mithilfe der Annotation @Mock für eine Methode mit übereinstimmender Signatur. Danach werden Aufrufe dieser Methode an unsere verspottete delegiert.

Wir können dies auch verwenden, um den Konstruktor einer Klasse zu verspotten, die bestimmte Argumente oder Konfigurationen benötigt, um Tests zu vereinfachen:

@Test public void testToMockUpDifficultConstructor() throws Exception{ new MockUp() { @Mock public void $init(Invocation invocation, String string) { ((AdvancedCollaborator)invocation.getInvokedInstance()).i = 1; } }; AdvancedCollaborator coll = new AdvancedCollaborator(null); assertEquals(1, coll.i); }

In diesem Beispiel sehen wir, dass Sie zum Verspotten von Konstruktoren die Methode $ init verspotten müssen . Sie können ein zusätzliches Argument vom Typ Invocation übergeben, mit dem Sie auf Informationen zum Aufruf der verspotteten Methode zugreifen können, einschließlich der Instanz, für die der Aufruf ausgeführt wird.

3.2. Mit Hilfe der Deencapsulation Klasse

JMockit enthält eine Testdienstprogrammklasse : die Deencapsulation . Wie der Name schon sagt, wird es verwendet, um einen Status eines Objekts zu entkapseln. Mit ihm können Sie das Testen vereinfachen, indem Sie auf Felder und Methoden zugreifen, auf die sonst nicht zugegriffen werden könnte.

Sie können eine Methode aufrufen:

@Test public void testToCallPrivateMethodsDirectly(){ Object value = Deencapsulation.invoke(mock, "privateMethod"); assertEquals("default:", value); }

Sie können auch Felder festlegen:

@Test public void testToSetPrivateFieldDirectly(){ Deencapsulation.setField(mock, "privateField", 10); assertEquals(10, mock.methodThatReturnsThePrivateField()); }

Und Felder bekommen:

@Test public void testToGetPrivateFieldDirectly(){ int value = Deencapsulation.getField(mock, "privateField"); assertEquals(5, value); }

Und erstellen Sie neue Instanzen von Klassen:

@Test public void testToCreateNewInstanceDirectly(){ AdvancedCollaborator coll = Deencapsulation .newInstance(AdvancedCollaborator.class, "foo"); assertEquals(3, coll.i); }

Sogar neue Instanzen innerer Klassen:

@Test public void testToCreateNewInnerClassInstanceDirectly(){ InnerCollaborator inner = Deencapsulation .newInnerInstance(InnerCollaborator.class, mock); assertNotNull(inner); }

Wie Sie sehen können, ist die Entkapselungsklasse beim Testen luftdichter Klassen äußerst nützlich. Ein Beispiel könnte sein, Abhängigkeiten einer Klasse festzulegen , die @ Autowired- Annotationen für private Felder verwendet und keine Setter für diese hat, oder innere Klassen zu testen, ohne von der öffentlichen Schnittstelle ihrer Containerklasse abhängig zu sein.

4. Verspotten mehrerer Schnittstellen in einem Mock

Nehmen wir an, Sie möchten eine noch nicht implementierte Klasse testen, wissen aber sicher, dass sie mehrere Schnittstellen implementiert.

Normalerweise können Sie diese Klasse vor der Implementierung nicht testen, aber mit JMockit können Sie Tests im Voraus vorbereiten, indem Sie mehr als eine Schnittstelle mit einem Scheinobjekt verspotten.

Dies kann erreicht werden, indem Generika verwendet und ein Typ definiert wird, der mehrere Schnittstellen erweitert. Dieser generische Typ kann entweder für eine ganze Testklasse oder nur für eine Testmethode definiert werden.

Zum Beispiel werden wir ein Modell für die Schnittstellen List und Comparable auf zwei Arten erstellen :

@RunWith(JMockit.class) public class AdvancedCollaboratorTest
    
     > { @Mocked private MultiMock multiMock; @Test public void testOnClass() { new Expectations() {{ multiMock.get(5); result = "foo"; multiMock.compareTo((List) any); result = 0; }}; assertEquals("foo", multiMock.get(5)); assertEquals(0, multiMock.compareTo(new ArrayList())); } @Test public 
     
      > void testOnMethod(@Mocked M mock) { new Expectations() {{ mock.get(5); result = "foo"; mock.compareTo((List) any); result = 0; }}; assertEquals("foo", mock.get(5)); assertEquals(0, mock.compareTo(new ArrayList())); } }
     
    

Wie Sie in Zeile 2 sehen können, können wir einen neuen Testtyp für den gesamten Test definieren, indem wir Generika für den Klassennamen verwenden. Auf diese Weise wird MultiMock als Typ verfügbar sein und Sie können Mocks dafür mit einer der Anmerkungen von JMockit erstellen.

In lines from 7 to 18, we can see an example using a mock of a multi-class defined for the whole test class.

If you need the multi-interface mock for just one test, you can achieve this by defining the generic type on the method signature and passing a new mock of that new generic as the test method argument. In lines 20 to 32, we can see an example of doing so for the same tested behavior as in the previous test.

5. Reusing Expectations and Verifications

In the end, when testing classes, you may encounter cases where you're repeating the same Expectations and/or Verifications over and over. To ease that, you can reuse both easily.

We're going to explain it by an example (we're using the classes Model, Collaborator, and Performer from our JMockit 101 article):

@RunWith(JMockit.class) public class ReusingTest { @Injectable private Collaborator collaborator; @Mocked private Model model; @Tested private Performer performer; @Before public void setup(){ new Expectations(){{ model.getInfo(); result = "foo"; minTimes = 0; collaborator.collaborate("foo"); result = true; minTimes = 0; }}; } @Test public void testWithSetup() { performer.perform(model); verifyTrueCalls(1); } protected void verifyTrueCalls(int calls){ new Verifications(){{ collaborator.receive(true); times = calls; }}; } final class TrueCallsVerification extends Verifications{ public TrueCallsVerification(int calls){ collaborator.receive(true); times = calls; } } @Test public void testWithFinalClass() { performer.perform(model); new TrueCallsVerification(1); } }

In this example, you can see in lines from 15 to 18 that we're preparing an expectation for every test so that model.getInfo() always returns “foo” and for collaborator.collaborate() to always expect “foo” as the argument and returning true. We put the minTimes = 0 statement so no fails appear when not actually using them in tests.

Also, we've created method verifyTrueCalls(int) to simplify verifications to the collaborator.receive(boolean) method when the passed argument is true.

Lastly, you can also create new types of specific expectations and verifications just extending any of Expectations or Verifications classes. Then you define a constructor if you need to configure the behavior and create a new instance of said type in a test as we do in lines from 33 to 43.

6. Conclusion

With this installment of the JMockit series, we have touched on several advanced topics that will definitely help you with everyday mocking and testing.

We may do more articles on JMockit, so stay tuned to learn even more.

Und wie immer finden Sie die vollständige Implementierung dieses Tutorials auf GitHub.

6.1. Artikel in der Reihe

Alle Artikel der Reihe:

  • JMockit 101
  • Ein Leitfaden zu JMockit-Erwartungen
  • Erweiterte JMockit-Verwendung