wake-up-neo.com

Testkurs mit einem neuen () Aufruf mit Mockito

Ich habe eine ältere Klasse, die einen neuen () - Aufruf enthält, um LoginContext () zu instanziieren:

public class TestedClass {
  public LoginContext login(String user, String password) {
    LoginContext lc = new LoginContext("login", callbackHandler);
  }
}

Ich möchte diese Klasse mit Mockito testen, um den LoginContext zu verspotten, da vor der Instantiierung die JAAS-Sicherheitsfunktionen eingerichtet werden müssen. Ich bin mir jedoch nicht sicher, wie dies geschehen soll, ohne die login () -Methode zu ändern, um den LoginContext zu externalisieren. Kann mit Mockito die LoginContext-Klasse simuliert werden?

43
bwobbones

Für die Zukunft würde ich die Antwort von Eran Harel empfehlen (Refactoring, new an die Fabrik, die verspottet werden kann). Wenn Sie jedoch den ursprünglichen Quellcode nicht ändern möchten, verwenden Sie eine sehr praktische und eindeutige Funktion: Spione. Aus der Dokumentation :

Sie können Spione von realen Objekten erstellen. Wenn Sie den Spion verwenden, werden die Methoden real aufgerufen (es sei denn, eine Methode wurde stubbed). 

Echte Spione sollten vorsichtig und gelegentlich verwendet werden, z. B. beim Umgang mit altem Code.

In Ihrem Fall sollten Sie schreiben:

TestedClass tc = spy(new TestedClass());
LoginContext lcMock = mock(LoginContext.class);
when(tc.login(anyString(), anyString())).thenReturn(lcMock);
45

Ich bin ganz für Eran Harels Lösung und in Fällen, in denen dies nicht möglich ist, ist Tomasz Nurkiewicz 'Vorschlag, Spionage zu betreiben, ausgezeichnet. Es ist jedoch erwähnenswert, dass es Situationen gibt, in denen keine zutreffen würde. Z.B. wenn die login Methode etwas "beefier" war:

public class TestedClass {
    public LoginContext login(String user, String password) {
        LoginContext lc = new LoginContext("login", callbackHandler);
        lc.doThis();
        lc.doThat();
    }
}

... und dies war ein alter Code, der nicht umgestaltet werden konnte, um die Initialisierung eines neuen LoginContext auf seine eigene Methode zu extrahieren und eine der oben genannten Lösungen anzuwenden.

Der Vollständigkeit halber sei noch eine dritte Methode erwähnt - mit PowerMock zum Einfügen des Scheinobjekts, wenn der Operator new aufgerufen wird. PowerMock ist jedoch keine Silberkugel. Es funktioniert durch die Anwendung von Byte-Code-Manipulation auf die Klassen, die es verspottet. Dies könnte eine zweifelhafte Übung sein, wenn die getesteten Klassen Byte-Code-Manipulationen oder -Reflexionen verwenden und zumindest aus meiner persönlichen Erfahrung bekannt, dass sie einen Leistungstreffer in den Test einführt. Wenn es keine anderen Optionen gibt, muss die einzige Option die gute Option sein:

@RunWith(PowerMockRunner.class)
@PrepareForTest(TestedClass.class)
public class TestedClassTest {

    @Test
    public void testLogin() {
        LoginContext lcMock = mock(LoginContext.class);
        whenNew(LoginContext.class).withArguments(anyString(), anyString()).thenReturn(lcMock);
        TestedClass tc = new TestedClass();
        tc.login ("something", "something else");
        // test the login's logic
    }
}
27
Mureinik

Sie können eine Factory verwenden, um den Anmeldekontext zu erstellen. Dann können Sie die Fabrik verspotten und alles zurückgeben, was Sie für den Test wünschen.

public class TestedClass {
  private final LoginContextFactory loginContextFactory;

  public TestedClass(final LoginContextFactory loginContextFactory) {
    this.loginContextFactory = loginContextFactory;
  }

  public LoginContext login(String user, String password) {
    LoginContext lc = loginContextFactory.createLoginContext();
  }
}

public interface LoginContextFactory {
  public LoginContext createLoginContext();
}
21
Eran Harel
    public class TestedClass {
    public LoginContext login(String user, String password) {
        LoginContext lc = new LoginContext("login", callbackHandler);
        lc.doThis();
        lc.doThat();
    }
  }

- Testklasse:

    @RunWith(PowerMockRunner.class)
    @PrepareForTest(TestedClass.class)
    public class TestedClassTest {

        @Test
        public void testLogin() {
            LoginContext lcMock = mock(LoginContext.class);
            whenNew(LoginContext.class).withArguments(anyString(), anyString()).thenReturn(lcMock);
//comment: this is giving mock object ( lcMock )
            TestedClass tc = new TestedClass();
            tc.login ("something", "something else"); ///  testing this method.
            // test the login's logic
        }
    }

Beim Aufruf der aktuellen Methode tc.login ("something", "something else"); aus dem testLogin () {- Dieser LoginContext-Wert lc wird auf null gesetzt und NPE wird beim Aufruf von lc.doThis(); ausgelöst.

4
sunjavax

Nicht dass ich wüsste, aber was tun Sie, wenn Sie eine Instanz von TestedClass erstellen, die Sie testen möchten:

TestedClass toTest = new TestedClass() {
    public LoginContext login(String user, String password) {
        //return mocked LoginContext
    }
};

Eine weitere Option wäre die Verwendung von Mockito, um eine Instanz von TestedClass zu erstellen und der überfälligen Instanz einen LoginContext zurückzugeben.

2
Kaj

In Situationen, in denen die zu testende Klasse geändert werden kann und es wünschenswert ist, Byte-Code-Manipulationen zu vermeiden, Dinge schnell zu halten oder Abhängigkeiten von Drittanbietern zu minimieren, möchte ich hier die Verwendung einer Factory zum Extrahieren der new-Operation verwenden. 

public class TestedClass {

    interface PojoFactory { Pojo getNewPojo(); }

    private final PojoFactory factory;

    /** For use in production - nothing needs to change. */
    public TestedClass() {
        this.factory = new PojoFactory() {
            @Override
            public Pojo getNewPojo() {
                return new Pojo();
            }
        };
    }

    /** For use in testing - provide a pojo factory. */
    public TestedClass(PojoFactory factory) {
        this.factory = factory;
    }

    public void doSomething() {
        Pojo pojo = this.factory.getNewPojo();
        anythingCouldHappen(pojo);
    }
}

Damit sind Ihre Test-, Assertions- und Check-Aufrufe für das Pojo-Objekt einfach:

public  void testSomething() {
    Pojo testPojo = new Pojo();
    TestedClass target = new TestedClass(new TestedClass.PojoFactory() {
                @Override
                public Pojo getNewPojo() {
                    return testPojo;
                }
            });
    target.doSomething();
    assertThat(testPojo.isLifeStillBeautiful(), is(true));
}

Der einzige Nachteil dieses Ansatzes besteht möglicherweise, wenn TestClass mehrere Konstruktoren enthält, die Sie mit dem zusätzlichen Parameter duplizieren müssten.

Aus SOLID Gründen möchten Sie wahrscheinlich die PojoFactory-Schnittstelle in die Pojo-Klasse und die Produktionsfabrik aufnehmen.

public class Pojo {

    interface PojoFactory { Pojo getNewPojo(); }

    public static final PojoFactory productionFactory = 
        new PojoFactory() {
            @Override 
            public Pojo getNewPojo() {
                return new Pojo();
            }
        };
1
Adam