110 votes

Tester une classe avec un appel new() avec Mockito

J'ai une ancienne classe 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 veux tester cette classe en utilisant Mockito pour simuler le LoginContext, car elle nécessite que la sécurité JAAS soit configurée avant l'instanciation, mais je ne sais pas comment faire sans modifier la méthode login() pour externaliser le LoginContext. Est-il possible d'utiliser Mockito pour simuler la classe LoginContext ?

92voto

Tomasz Nurkiewicz Points 140462

Pour l'avenir, je recommanderais La réponse d'Eran Harel (refactoring moving new à une usine qui peut être simulée). Mais si vous ne voulez pas modifier le code source original, utilisez une fonction très pratique et unique : espions . De la documentation :

Vous pouvez créer des espions à partir d'objets réels. Lorsque vous utilisez l'espion, le réel sont appelées (à moins qu'une méthode ait été stubée).

Il faut utiliser de vrais espions soigneusement et occasionnellement par exemple lorsqu'il s'agit d'un 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);

76voto

Mureinik Points 61228

Je suis tout à fait pour La solution d'Eran Harel et dans les cas où cela n'est pas possible, La suggestion de Tomasz Nurkiewicz pour l'espionnage est excellent. Toutefois, il convient de noter qu'il existe des situations où ni l'un ni l'autre ne s'applique. Par exemple, si le login était un peu plus "musclée" :

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

... et il s'agissait d'un vieux code qui ne pouvait pas être remanié pour extraire l'initialisation d'une nouvelle 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 - l'utilisation de PowerMock pour injecter l'objet fantaisie lorsque le new est appelé. PowerMock n'est pas une solution miracle, cependant. Il fonctionne en appliquant la manipulation du byte-code sur les classes qu'il asservit, ce qui pourrait être une pratique douteuse si les classes testées utilisent la manipulation du byte-code ou la réflexion et, du moins d'après mon expérience personnelle, est connu pour introduire un impact sur les performances du test. Mais encore une fois, 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
    }
}

36voto

Eran Harel Points 1175

Vous pouvez utiliser une fabrique pour créer le contexte de connexion. Ensuite, vous pouvez simuler la fabrique et renvoyer 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();
}

5voto

sunjavax Points 91
    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 du testLogin() { - Ce LoginContext lc est défini comme nul et génère un NPE lors de l'appel de la fonction lc.doThis();

4voto

Kaj Points 6802

Pas à ma connaissance, mais que diriez-vous de faire quelque chose comme ceci lorsque vous créez une instance de TestedClass que vous voulez 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 retourner un LoginContext.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X