web-dev-qa-db-fra.com

Testez la classe avec un appel new () avec Mockito

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?

43
bwobbones

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);
45

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
    }
}
27
Mureinik

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();
}
21
Eran Harel
    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();.

4
sunjavax

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.

2
Kaj

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();
            }
        };
1
Adam