J'ai une classe héritée qui contient un appel new () pour instancier un LoginContext ():
public class TestedClass {
public LoginContext login(String user, String password) {
LoginContext lc = new LoginContext("login", callbackHandler);
}
}
Je souhaite tester cette classe en utilisant Mockito pour simuler le LoginContext car il nécessite que les éléments de sécurité JAAS soient configurés avant l'instanciation, mais je ne sais pas comment faire cela sans changer la méthode login () pour externaliser le LoginContext. Est-il possible d'utiliser Mockito pour se moquer de la classe LoginContext?
Pour l'avenir, je recommanderais la réponse d'Eran Harel (refactoring déplaçant new
dans une usine pouvant être falsifiée). Mais si vous ne souhaitez pas modifier le code source d'origine, utilisez une fonctionnalité unique et très pratique: espions. De la documentation :
Vous pouvez créer des espions d'objets réels. Lorsque vous utilisez l'espion, les méthodes réelles sont appelées (sauf si une méthode a été stubée).
Les vrais espions doivent être utilisés avec précaution et occasionnellement, par exemple pour traiter du code hérité.
Dans votre cas, vous devriez écrire:
TestedClass tc = spy(new TestedClass());
LoginContext lcMock = mock(LoginContext.class);
when(tc.login(anyString(), anyString())).thenReturn(lcMock);
Je suis tout à fait en faveur de la solution d'Eran Harel et dans les cas où cela n'est pas possible, la suggestion de Tomasz Nurkiewicz d'espionnage est excellente. Cependant, il convient de noter qu'il existe des situations dans lesquelles ni l'une ni l'autre ne s'appliquerait. Par exemple. si la méthode login
était un peu "plus costaud":
public class TestedClass {
public LoginContext login(String user, String password) {
LoginContext lc = new LoginContext("login", callbackHandler);
lc.doThis();
lc.doThat();
}
}
... et c’était un vieux code qui ne pouvait pas être refactorisé pour extraire l’initialisation d’un nouveau LoginContext
à sa propre méthode et appliquer l’une des solutions susmentionnées.
Par souci d'exhaustivité, il convient de mentionner une troisième technique - en utilisant PowerMock pour injecter l'objet fictif lorsque l'opérateur new
est appelé. PowerMock n'est cependant pas une solution miracle. Il fonctionne en appliquant une manipulation de code octet sur les classes qu’il simule, ce qui peut être une pratique louche si les classes testées font appel à la manipulation de code octet ou à la réflexion et au moins de par mon expérience personnelle, est connue pour introduire un impact sur les performances. Là encore, s'il n'y a pas d'autres options, la seule option doit être la bonne:
@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
}
}
Vous pouvez utiliser une fabrique pour créer le contexte de connexion. Ensuite, vous pouvez vous moquer de l'usine et retourner ce que vous voulez pour votre test.
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();
}
public class TestedClass {
public LoginContext login(String user, String password) {
LoginContext lc = new LoginContext("login", callbackHandler);
lc.doThis();
lc.doThat();
}
}
- Classe de test:
@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
}
}
Lors de l’appel de la méthode actuelle tc.login ("something", "something else");
à partir de testLogin () {- Ce LoginContext lc est défini sur null et renvoie NPE lors de l’appel lc.doThis();
.
Pas que je sache, mais qu'en est-il de faire quelque chose comme ceci lorsque vous créez une instance de TestedClass que vous souhaitez tester:
TestedClass toTest = new TestedClass() {
public LoginContext login(String user, String password) {
//return mocked LoginContext
}
};
Une autre option serait d’utiliser Mockito pour créer une instance de TestedClass et laisser l’instance simulée renvoyer un LoginContext.
Dans les situations où la classe en cours de test peut être modifiée et où il est souhaitable d'éviter la manipulation de code octet, de garder les choses rapidement ou de minimiser les dépendances de tiers, voici mon point de vue sur l'utilisation d'une usine pour extraire l'opération new
.
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);
}
}
Avec ceci en place, vos tests, vos assertions et vos vérifications sur l’objet Pojo sont simples:
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));
}
Le seul inconvénient de cette approche survient potentiellement si TestClass
a plusieurs constructeurs que vous devez dupliquer avec le paramètre extra.
Pour des raisons SOLID, vous souhaiterez probablement placer l'interface PojoFactory sur la classe Pojo, ainsi que sur l'usine de production.
public class Pojo {
interface PojoFactory { Pojo getNewPojo(); }
public static final PojoFactory productionFactory =
new PojoFactory() {
@Override
public Pojo getNewPojo() {
return new Pojo();
}
};