1
votes

@Value sur un champ dans un @Service injecté avec @MockBean donne une valeur nulle

J'ai ce service:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = MyApplication.class)
public class MyControllerTest {

    private MockMvc myControllerMockMvc;

    @Autowired
    private MyController myController; // This controller injects an instance of MyService

    @MockBean
    private MyService myServiceMock;

    @Before
    public void setup() {
        this.myControllerMockMvc = MockMvcBuilders.standaloneSetup(myController).build();

        when(myServiceMock.someMethodWhichUsesMyValueField()).thenCallRealMethod();
        // Also mock the other methods in myServiceMock that call the external webservices
    }


    @Test
    public void someTest() throws Exception {
        // use the myControllerMockMvc to call a POST method that calls myService.someMethodWhichUsesMyValueField()
    }
}

Et ce test d'intégration:

@Service
public class MyService {

    @Value("${my.value}")
    private String myValue;

    public String someMethodWhichUsesMyValueField() {
        return myValue;
    }

    // Also contains other methods that use some external services accessed with a `RestTemplate`
}

Le problème est que lorsque myService.someMethodWhichUsesMyValueField () est appelé depuis la méthode MyController appelée depuis la méthode someTest () , le champ myValue (et tout le champ annoté avec @Autowired ) est null même si mon application.properties définit correctement my.value = some value .

Existe-t-il un moyen pour que myValue injecte correctement la valeur décrite dans application.properties comme n'importe quel composant @Autowired normal ?


1 commentaires

@Samim non cela ne fonctionne pas car ici myServiceMock est injecté avec @MockBean qui ne parvient pas à inclure les valeurs @Value


3 Réponses :


0
votes

Vous pouvez définir vos propriétés dans une classe de test

@TestPropertySource(properties = {"my.value=value"})
public class MyControllerTest {
//
}


3 commentaires

J'ai essayé d'ajouter cette annotation sur mon MyControllerTest sans aucun effet: myValue à l'intérieur de myService était toujours null .


@AnthonyO. vous utilisez @MockBean MyService myServiceMock . Cela signifie que le ressort crée une maquette au lieu d'un objet. Vous devez le filer automatiquement


OK mais ce n'est pas votre réponse, n'est-ce pas? J'ai essayé d'ajouter @Autowired sur le champ myServiceMock dans MyControllerTest sans plus de succès. J'ai aussi essayé d'ajouter un champ @Autowired MyService myService en plus de mon @MockBean MyService myServiceMock mais alors ces 2 objets sont la même instance: le simulé.



0
votes

Utilisez un @SpyBean au lieu d'un @MockBean car vous aurez alors le véritable objet Spring injecté avec une méthode que vous pouvez vous moquer (merci pour réponse de @thomas-andolf pour l'astuce @Spy ):

// [...] Nothing to change in annotations
public class MyControllerTest {

    // [...] Nothing to change in the other fields

    @Value("${my.value}")
    private String myValue;

    @Before
    public void setup() throws NoSuchFieldException, IllegalAccessException {
        ReflectionTestUtils.setField(myServiceMock, "myValue", myValue); // thanks to @thomas-andolf's [answer](https://stackoverflow.com/a/55148170/535203)
        // // Equivalent with traditional reflection tools:
        // Field myValueField = MyService.class.getDeclaredField("myValue");
        // myValueField.setAccessible(true);
        // myValueField.set(myServiceMock, myValue);

        // [...] the rest of the original method
    }

    // [...] Nothing to change in the test methods
}

L'autre façon (moche?) Est d'injecter le champ manuellement directement depuis la classe de test, comme ceci par exemple:

// [...] Nothing to change in annotations
public class MyControllerTest {

    // [...] Nothing to change in the other fields

    @SpyBean
    private MyService myServiceMock;

    @Before
    public void setup() throws NoSuchFieldException, IllegalAccessException {
        // [...] Nothing to change before mocking
        // Then, do not mock anymore with .thenCallRealMethod() which is the default behavior with the spy
        // And when mocking the other methods in myServiceMock that call the external webservices, use this kind of declaration:
        doAnswer(invocation -> /* reply something */).when(myServiceMock).someMethodWhichUsesExternalWebService(any());
        // instead of when(myServiceMock.someMethodWhichUsesExternalWebService(any())).thenAnswer(invocation -> /* reply something */);
    }

    // [...] Nothing to change in the test methods
}


0 commentaires

0
votes

Voici quelques façons de résoudre ce problème, vous pouvez soit utiliser:

Injection de constructeur:

ReflectionTestUtils.setField(myService, "myValue", "FooBar");

Et au lieu d'utiliser un simulacre, utilisez simplement la classe et lui donner sa valeur.

Ou vous pouvez utiliser:

@Service
public class MyService {

    private final myValue;

    public MyService(@Value("${my.value}") myValue) {
        this.myValue = myValue;
    }

    public String someMethodWhichUsesMyValueField() {
        return myValue;
    }
}

pour définir la propriété myValue sur foobar.

Ou vous pouvez le changer pour utiliser un @Spy à la place, qui est une "simulation partielle". Un espion est la classe d'origine avec ses méthodes originales et vous pouvez alors choisir de vous moquer de certaines méthodes mais de conserver la logique de la classe d'origine dans certaines méthodes.


0 commentaires