Je suis assez nouveau pour Mockito et j'ai du mal à nettoyer.
J'utilisais JMock2 pour les tests unitaires. Pour autant que je sache, JMock2 préserve les attentes et autres informations fictives dans un contexte qui sera reconstruit pour chaque méthode de test. Par conséquent, chaque méthode d'essai n'est pas gênée par les autres.
J'ai adopté la même stratégie pour les tests de printemps lors de l'utilisation de JMock2, j'ai trouvé un problème potentiel avec les stratégies que j'ai utilisées dans mon post : Le contexte d'application est reconstruit pour chaque méthode de test et ralentit donc toute la procédure de test.
J'ai remarqué que de nombreux articles recommandent d'utiliser Mockito dans les tests de printemps et je voudrais essayer. Cela fonctionne bien jusqu'à ce que j'écrive deux méthodes de test dans un scénario de test. Chaque méthode de test a réussi lorsqu'elle a fonctionné seule. L'une d'elles a échoué si elle fonctionnait ensemble. J'ai supposé que cela était dû au fait que les informations factices étaient conservées dans la simulation elle-même (car je ne vois aucun objet de contexte comme celui-ci dans JMock) et que la simulation (et le contexte d'application) est partagée dans les deux méthodes de test.
Je l'ai résolu en ajoutant reset () dans la méthode @Before. Ma question est quelle est la meilleure pratique pour gérer cette situation (le javadoc de reset () dit que le code est odeur si vous avez besoin de reset ())? Toute idée est appréciée, merci d'avance.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
"file:src/main/webapp/WEB-INF/booking-servlet.xml",
"classpath:test-booking-servlet.xml" })
@WebAppConfiguration
public class PlaceOrderControllerIntegrationTests implements IntegrationTests {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Autowired
private PlaceOrderService placeOrderService;
@Before
public void setup() {
this.mockMvc = webAppContextSetup(this.wac).build();
reset(placeOrderService);// reset mock
}
@Test
public void fowardsToFoodSelectionViewAfterPendingOrderIsPlaced()
throws Exception {
final Address deliveryAddress = new AddressFixture().build();
final String deliveryTime = twoHoursLater();
final PendingOrder pendingOrder = new PendingOrderFixture()
.with(deliveryAddress).at(with(deliveryTime)).build();
when(placeOrderService.placeOrder(deliveryAddress, with(deliveryTime)))
.thenReturn(pendingOrder);
mockMvc.perform(...);
}
@Test
public void returnsToPlaceOrderViewWhenFailsToPlaceOrder() throws Exception {
final Address deliveryAddress = new AddressFixture().build();
final String deliveryTime = twoHoursLater();
final PendingOrder pendingOrder = new PendingOrderFixture()
.with(deliveryAddress).at(with(deliveryTime)).build();
NoAvailableRestaurantException noAvailableRestaurantException = new NoAvailableRestaurantException(
deliveryAddress, with(deliveryTime));
when(placeOrderService.placeOrder(deliveryAddress, with(deliveryTime)))
.thenThrow(noAvailableRestaurantException);
mockMvc.perform(...);
}
À propos du placement de la réinitialisation après la méthode de test
Je pense que la réinitialisation des simulations devrait être effectuée après la méthode de test, car cela implique qu'il y a effectivement quelque chose qui s'est passé pendant le test qui doit être nettoyé.
Si la réinitialisation est effectuée avant la méthode de test, je ne serais pas sûr, qu'est-ce qui s'est passé avant le test qui devrait être réinitialisé? Qu'en est-il des objets non fictifs? Y a-t-il une raison (peut-être qu'il y en a) à cela? S'il y a une raison pour laquelle ce n'est pas mentionné dans le code (par exemple le nom de la méthode)? Etc.
Pas un fan des tests basés sur Spring
Contexte
Utiliser Spring, c'est comme abandonner les tests unitaires d'une classe; avec Spring, vous avez moins de contrôle sur le test: isolation, instanciation, cycle de vie, pour citer quelques-uns des éléments dans un test unitaire. Cependant, dans de nombreux cas, Spring propose des bibliothèques et un cadre qui ne sont pas "" transparents ", pour tester, il vaut mieux tester le comportement réel de l'ensemble, comme avec Spring MVC, Spring Batch, etc.
Et la création de ces tests est beaucoup plus gênante car elle impose dans de nombreux cas au développeur de créer des tests d'intégration pour tester sérieusement le comportement du code de production. Comme de nombreux développeurs ne connaissent pas tous les détails de la façon dont votre code vit dans Spring, cela peut conduire à de nombreuses surprises pour essayer de tester une classe avec des tests unitaires.
Mais les ennuis continuent, les tests doivent être rapides et petits pour donner un retour rapide aux développeurs (le plugin IDE tel que Infinitest est parfait pour ça), mais les tests avec Spring sont intrinsèquement plus lent et consommant plus de mémoire. Ce qui a tendance à les exécuter moins souvent et même à les éviter totalement sur le poste de travail local ... pour découvrir plus tard sur le serveur CI qu'ils échouent.
Cycle de vie avec Mockito et Spring
Donc, quand un test d'intégration est conçu pour un sous-système, vous vous retrouvez avec de nombreux objets et évidemment les collaborateurs, qui sont probablement moqués. Le cycle de vie est contrôlé par le Spring Runner, mais les maquettes Mockito ne le sont pas. Vous devez donc gérer vous-même le cycle de vie des simulacres.
Encore une fois à propos du cycle de vie pendant un projet avec Spring Batch, nous avons eu quelques problèmes avec des effets résiduels sur les non-mocks donc nous avions deux choix, ne faire qu'une seule méthode de test par classe de test ou utiliser l'astuce contexte sale : @DirtiesContext(classMode=ClassMode.AFTER_EACH_TEST_METHOD)
. Cela a conduit à des tests plus lents, à une consommation de mémoire plus élevée, mais c'était la meilleure option que nous avions. Avec cette astuce, vous n'aurez pas à réinitialiser les mocks Mockito.
Une lumière possible dans l'obscurité
Je ne connais pas assez bien le projet, mais springockito peut vous fournir un peu de sucre sur le cycle de vie. Le sous-projet d'annotation semble être encore meilleur: il semble permettre à Spring de gérer le cycle de vie des beans dans le conteneur Spring et de laisser le test contrôler comment les mocks sont utilisés. Je n'ai toujours pas d'expérience avec cet outil, donc il pourrait y avoir des surprises.
Comme avertissement, j'aime beaucoup Spring, il offre de nombreux outils remarquables pour simplifier l'utilisation d'autres frameworks, il peut augmenter la productivité, il aide à la conception, mais comme tous les outils inventés par les humains, il y a toujours un bord rugueux (sinon plus ... ).
En passant, il est intéressant de voir que cette problématique se trouve dans un contexte JUnit , car JUnit instancie la classe de test pour chaque méthode de test. Si le test était basé sur TestNG alors l'approche pourrait être un peu différente car TestNG ne crée qu'une seule instance de la classe de test, les champs fictifs restants seraient obligatoires indépendamment de l'utilisation de Spring.
Ancienne réponse:
Je ne suis pas un grand fan de l'utilisation de maquettes Mockito dans un texte de printemps. Mais pourriez-vous chercher quelque chose comme:
@After public void reset_mocks() {
Mockito.reset(placeOrderService);
}
Spring Boot a @MockBean
annotation que vous pouvez utiliser pour simuler votre service. Vous n'avez plus besoin de réinitialiser les mocks manuellement. Remplacez simplement @Autowired
par @MockBean
:
@MockBean
private PlaceOrderService placeOrderService;
Le test basé sur le printemps est difficile à réaliser rapidement et indépendamment (comme @Brice écrit ). Voici une petite méthode utilitaire pour réinitialiser toutes les simulations (vous devez l'appeler manuellement dans chaque @Before
méthode):
import org.mockito.Mockito;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.ApplicationContext;
public class MyTest {
public void resetAll(ApplicationContext applicationContext) throws Exception {
for (String name : applicationContext.getBeanDefinitionNames()) {
Object bean = applicationContext.getBean(name);
if (AopUtils.isAopProxy(bean) && bean instanceof Advised) {
bean = ((Advised)bean).getTargetSource().getTarget();
}
if (Mockito.mockingDetails(bean).isMock()) {
Mockito.reset(bean);
}
}
}
}
Comme vous le voyez, il existe une itération pour tous les beans, vérifiez si le bean est fictif ou non, et réinitialisez le mock. Je porte une attention particulière sur appel AopUtils.isAopProxy
et ((Advised)bean).getTargetSource().getTarget()
. Si votre bean contient un @Transactional
annoter la maquette de ce bean toujours enveloppée par spring dans un objet proxy, donc pour réinitialiser ou vérifier cette maquette, vous devez d'abord la déballer. Sinon, vous obtiendrez un UnfinishedVerificationException
qui peut apparaître de temps en temps dans différents tests.
Dans mon cas AopUtils.isAopProxy
est assez. Mais il y a aussi AopUtils.isCglibProxy
et AopUtils.isJdkDynamicProxy
si vous rencontrez des problèmes avec le proxy.
mockito est 1.10.19
ressort-test est 3.2.2.RELEASE
Au lieu d'injecter l'objet placeOrderService
, vous devriez probablement laisser Mockito l'initialiser en tant que @Mock
avant chaque test, via quelque chose comme ceci:
@Mock private PlaceOrderService placeOrderService;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
}
Comme recommandé dans le Javadoc ici: http://docs.mockito.googlecode.com/hg/latest/org/mockito/MockitoAnnotations.html
Vous pouvez même mettre le @Before
méthode dans une superclasse et juste l'étendre pour chaque classe de cas de test qui utilise @Mock
objets.