web-dev-qa-db-fra.com

Injection de champ privé @Autowired lors des tests

J'ai une configuration de composant qui est essentiellement un lanceur pour une application. Il est configuré comme suit:

@Component
public class MyLauncher {
    @Autowired
    MyService myService;

    //other methods
}

MyService est annoté avec l'annotation Spring @Service Et est automatiquement câblé dans ma classe de lancement sans aucun problème.

Je voudrais écrire quelques cas de test jUnit pour MyLauncher. Pour ce faire, j'ai commencé un cours comme celui-ci:

public class MyLauncherTest
    private MyLauncher myLauncher = new MyLauncher();

    @Test
    public void someTest() {

    }
}

Puis-je créer un objet Mock pour MyService et l'injecter dans myLauncher dans ma classe de test? À l'heure actuelle, je n'ai ni getter ni paramètre dans myLauncher, car Spring gère le câblage automatique. Si possible, j'aimerais ne pas avoir à ajouter des accesseurs et des setters. Puis-je dire au scénario de test d'injecter un objet fantaisie dans la variable autowired à l'aide d'une méthode init @Before?

Si je me trompe complètement, n'hésitez pas à le dire. Je suis encore nouveau pour ça. Mon objectif principal est d’avoir juste du code ou une annotation Java qui place un objet fictif dans cette variable @Autowired Sans que je doive écrire une méthode de définition ou utiliser un applicationContext-test.xml. Je préférerais de beaucoup gérer tout le contenu des tests dans le fichier .Java Au lieu de devoir conserver un contenu distinct pour les tests.

J'espère utiliser Mockito pour les objets fictifs. Dans le passé, je l’avais fait en utilisant org.mockito.Mockito Et en créant mes objets avec Mockito.mock(MyClass.class).

61
Kyle

Vous pouvez absolument injecter des simulacres sur MyLauncher dans votre test. Je suis sûr que si vous montrez quel cadre moqueur vous utilisez, quelqu'un répondrait rapidement. Avec mockito, je voudrais utiliser @RunWith (MockitoJUnitRunner.class) et des annotations pour myLauncher. Cela ressemblerait à ce qui est en dessous.

@RunWith(MockitoJUnitRunner.class)
public class MyLauncherTest
    @InjectMocks
    private MyLauncher myLauncher = new MyLauncher();

    @Mock
    private MyService myService;

    @Test
    public void someTest() {

    }
}
70
Manuel Quinones

La réponse acceptée (utilisez MockitoJUnitRunner et @InjectMocks) est super. Mais si vous voulez quelque chose d'un peu plus léger (pas de coureur JUnit spécial), et moins "magique" (plus transparent) surtout pour une utilisation occasionnelle, vous pouvez simplement définir les champs privés directement à l'aide de l'introspection.

Si vous utilisez Spring, vous avez déjà une classe d’utilitaires pour cela: org.springframework.test.util.ReflectionTestUtils

L'utilisation est assez simple:

ReflectionTestUtils.setField(myLauncher, "myService", myService);

Le premier argument est votre bean cible, le second est le nom du champ (généralement privé) et le dernier est la valeur à injecter.

Si vous n'utilisez pas Spring, il est assez simple d'implémenter une telle méthode utilitaire. Voici le code que j'ai utilisé avant d'avoir trouvé ce cours de printemps:

public static void setPrivateField(Object target, String fieldName, Object value){
        try{
            Field privateField = target.getClass().getDeclaredField(fieldName);
            privateField.setAccessible(true);
            privateField.set(target, value);
        }catch(Exception e){
            throw new RuntimeException(e);
        }
    }
35
Pierre Henry

Parfois, vous pouvez refactoriser votre @Component Pour utiliser une injection basée sur le constructeur ou le setter pour configurer votre test (vous pouvez toujours vous fier à @Autowired). Maintenant, vous pouvez créer votre test entièrement sans un framework moqueur en implémentant des talons de test (par exemple, Martin Fowler's MailServiceStub ):

@Component
public class MyLauncher {

    private MyService myService;

    @Autowired
    MyLauncher(MyService myService) {
        this.myService = myService;
    }

    // other methods
}

public class MyServiceStub implements MyService {
    // ...
}

public class MyLauncherTest
    private MyLauncher myLauncher;
    private MyServiceStub myServiceStub;

    @Before
    public void setUp() {
        myServiceStub = new MyServiceStub();
        myLauncher = new MyLauncher(myServiceStub);
    }

    @Test
    public void someTest() {

    }
}

Cette technique est particulièrement utile si le test et la classe sous test se trouvent dans le même package, car vous pouvez utiliser le modificateur d'accès par défaut, package-private pour empêcher les autres classes d'y accéder. Notez que vous pouvez toujours avoir votre code de production dans src/main/Java Mais vos tests dans les répertoires src/main/test.


Si vous aimez Mockito, alors vous apprécierez le MockitoJUnitRunner . Cela vous permet de faire des choses "magiques" comme @Manuel vous a montré:

@RunWith(MockitoJUnitRunner.class)
public class MyLauncherTest
    @InjectMocks
    private MyLauncher myLauncher; // no need to call the constructor

    @Mock
    private MyService myService;

    @Test
    public void someTest() {

    }
}

Vous pouvez également utiliser le coureur JUnit par défaut et appeler le MockitoAnnotations.initMocks () dans une méthode setUp() pour laisser Mockito initialiser les valeurs annotées. Vous pouvez trouver plus d'informations dans le javadoc de @ InitMocks et dans un article de blog que j'ai écrit.

14
matsev

Je crois que pour que le câblage automatique fonctionne sur votre classe MyLauncher (pour myService), vous devrez laisser Spring l'initialiser au lieu d'appeler le constructeur, en câblant automatiquement myLauncher. Une fois que ce dernier est automatiquement câblé (et que myService est également auto-câblé), Spring (1.4.0 et versions ultérieures) fournit une annotation @MockBean que vous pouvez mettre dans votre test. Cela remplacera un seul haricot correspondant dans le contexte par un modèle de ce type. Vous pouvez ensuite définir plus précisément ce que vous voulez vous moquer, dans une méthode @Avant.

public class MyLauncherTest
    @MockBean
    private MyService myService;

    @Autowired
    private MyLauncher myLauncher;

    @Before
    private void setupMockBean() {
        doNothing().when(myService).someVoidMethod();
        doReturn("Some Value").when(myService).someStringMethod();
    }

    @Test
    public void someTest() {
        myLauncher.doSomething();
    }
}

Votre classe MyLauncher peut alors rester inchangée et votre bean MyService sera une maquette dont les méthodes renverront des valeurs telles que vous les avez définies:

@Component
public class MyLauncher {
    @Autowired
    MyService myService;

    public void doSomething() {
        myService.someVoidMethod();
        myService.someMethodThatCallsSomeStringMethod();
    }

    //other methods
}

Quelques avantages de cela par rapport aux autres méthodes mentionnées sont les suivants:

  1. Vous n'avez pas besoin d'injecter manuellement myService.
  2. Vous n'avez pas besoin d'utiliser le coureur ou les règles de Mockito.
1
snydergd

Regardez ceci lien

Puis écrivez votre cas de test en tant que

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"/applicationContext.xml"})
public class MyLauncherTest{

@Resource
private MyLauncher myLauncher ;

   @Test
   public void someTest() {
       //test code
   }
}

Je suis un nouvel utilisateur pour Spring. J'ai trouvé une solution différente pour cela. Utiliser la réflexion, rendre publics les champs nécessaires et assigner des objets fictifs.

Ceci est mon contrôleur d'authentification et il possède des propriétés privées Autowired.

@RestController
public class AuthController {

    @Autowired
    private UsersDAOInterface usersDao;

    @Autowired
    private TokensDAOInterface tokensDao;

    @RequestMapping(path = "/auth/getToken", method = RequestMethod.POST)
    public @ResponseBody Object getToken(@RequestParam String username,
            @RequestParam String password) {
        User user = usersDao.getLoginUser(username, password);

        if (user == null)
            return new ErrorResult("Kullanıcıadı veya şifre hatalı");

        Token token = new Token();
        token.setTokenId("aergaerg");
        token.setUserId(1);
        token.setInsertDatetime(new Date());
        return token;
    }
}

Et ceci est mon test Junit pour AuthController. Je rends publiques les propriétés privées dont j'ai besoin et leur assigne des objets fictifs et les berce :)

public class AuthControllerTest {

    @Test
    public void getToken() {
        try {
            UsersDAO mockUsersDao = mock(UsersDAO.class);
            TokensDAO mockTokensDao = mock(TokensDAO.class);

            User dummyUser = new User();
            dummyUser.setId(10);
            dummyUser.setUsername("nixarsoft");
            dummyUser.setTopId(0);

            when(mockUsersDao.getLoginUser(Matchers.anyString(), Matchers.anyString())) //
                    .thenReturn(dummyUser);

            AuthController ctrl = new AuthController();

            Field usersDaoField = ctrl.getClass().getDeclaredField("usersDao");
            usersDaoField.setAccessible(true);
            usersDaoField.set(ctrl, mockUsersDao);

            Field tokensDaoField = ctrl.getClass().getDeclaredField("tokensDao");
            tokensDaoField.setAccessible(true);
            tokensDaoField.set(ctrl, mockTokensDao);

            Token t = (Token) ctrl.getToken("test", "aergaeg");

            Assert.assertNotNull(t);

        } catch (Exception ex) {
            System.out.println(ex);
        }
    }

}

Je ne connais pas les avantages et les inconvénients de cette façon, mais cela fonctionne. Cette technique a un peu plus de code, mais ces codes peuvent être séparés par différentes méthodes, etc. Il y a plus de bonnes réponses à cette question, mais je veux indiquer une solution différente. Désolé pour mon mauvais anglais. Avoir un bon Java à tout le monde :)

0
kodmanyagha