2009-02-19 25 views
49

Nous utilisons Spring pour mes applications et Spring Testing pour les tests unitaires. Nous avons cependant un petit problème: le code de l'application charge un contexte d'application Spring à partir d'une liste d'emplacements (fichiers xml) dans le classpath. Mais lorsque nous effectuons nos tests unitaires, nous voulons que certains des beans de Spring soient des mocks au lieu de classes d'implémentation à part entière. De plus, pour certains tests unitaires, nous voulons que certains haricots fassent l'objet de simulacres, alors que pour d'autres tests unitaires, nous voulons que les autres fèves deviennent des simulacres, car nous testons différentes couches de l'application. Tout cela signifie que je veux redéfinir des beans spécifiques du contexte d'application et actualiser le contexte si vous le souhaitez. En faisant cela, je veux redéfinir seulement une petite partie des beans situés dans un (ou plusieurs) fichier de définition de beans xml d'origine. Je ne peux pas trouver un moyen facile de le faire. Il est toujours considéré que le printemps est un test d'unité amicale, donc il me manque quelque chose ici.Redéfinition des haricots de printemps dans l'environnement de test unitaire

Avez-vous des idées pour le faire?

Merci.

+8

Le ressort rend les tests unitaires simples. C'est la partie qui vous manque - vous faites un test d'intégration et non un test unitaire. Dans un vrai test unitaire, tous les beans dépendants devraient être des simulacres, car vous testez uniquement une unité et non un système entier. – bpapa

+0

Comment * test unitaire * une application de printemps: http://confessionsofanagilecoach.blogspot.com/2016/05/unit-testing-spring-applications.html –

Répondre

18

je propose une TestClass personnalisée et quelques règles faciles pour les emplacements du ressort bean.xml

@RunWith(SpringJUnit4ClassRunner.class) 
@ContextConfiguration(locations = { 
    "classpath*:spring/*.xml", 
    "classpath*:spring/persistence/*.xml", 
    "classpath*:spring/mock/*.xml"}) 
@Transactional 
@TestExecutionListeners({ 
    DependencyInjectionTestExecutionListener.class, 
    TransactionalTestExecutionListener.class, 
    DirtiesContextTestExecutionListener.class}) 
public abstract class AbstractHibernateTests implements ApplicationContextAware 
{ 

    /** 
    * Logger for Subclasses. 
    */ 
    protected final Logger LOG = LoggerFactory.getLogger(getClass()); 

    /** 
    * The {@link ApplicationContext} that was injected into this test instance 
    * via {@link #setApplicationContext(ApplicationContext)}. 
    */ 
    protected ApplicationContext applicationContext; 

    /** 
    * Set the {@link ApplicationContext} to be used by this test instance, 
    * provided via {@link ApplicationContextAware} semantics. 
    */ 
    @Override 
    public final void setApplicationContext(
      final ApplicationContext applicationContext) { 
     this.applicationContext = applicationContext; 
    } 
} 

s'il y a mock- bean.xml à l'emplacement spécifié, ils remplaceront tous les "vrais" bean.xml dans les emplacements "normaux" - vos emplacements normaux peuvent différer

mais ...Je ne mélangerais jamais des haricots mock et non-simulés il est difficile de tracer les problèmes, quand l'application vieillit.

3

Facile. Vous utilisez un contexte d'application personnalisé pour vos tests unitaires. Ou vous n'en utilisez pas du tout et vous créez et injectez manuellement vos haricots.

Il me semble que vos tests pourraient être un peu trop larges. Les tests unitaires consistent à tester, bien, les unités. Un haricot de printemps est un très bon exemple d'une unité. Vous ne devriez pas avoir besoin d'un contexte d'application complet pour cela. Je trouve que si vos tests unitaires sont d'un niveau si élevé que vous avez besoin de centaines de haricots, de connexions à la base de données, etc., vous avez un test unitaire très fragile qui va se casser au prochain changement, qui sera difficile à maintenir. t ajoutant beaucoup de valeur.

+0

Vous avez raison, nous utilisons plutôt des tests de système et non des tests unitaires , nous testons le flux de l'API vers la base de données sur notre serveur d'applications. Je réalise que le test d'unité d'écriture est un meilleur moyen, mais ce n'est pas faisable en ce moment. – Stas

0

Peut-être pourriez-vous utiliser des qualificatifs pour vos haricots? Vous devez redéfinir les haricots que vous souhaitez mocker dans un contexte d'application distinct et les étiqueter avec un qualificatif «test». Dans vos tests unitaires, lors du câblage de vos haricots, spécifiez toujours le qualificatif "test" pour utiliser les maquettes.

2

Vous pouvez utiliser la fonctionnalité import dans le contexte de votre application de test pour charger les beans prod et remplacer ceux que vous voulez. Par exemple, ma source de données prod est généralement acquise via la recherche JNDI, mais lorsque je teste, j'utilise une source de données DriverManager, donc je n'ai pas besoin de démarrer le serveur d'applications pour tester.

4

Vous pouvez également écrire vos tests unitaires pour ne pas exiger du tout des recherches:

@ContextConfiguration(locations = { "classpath:/path/to/test-config.xml" }) 
@RunWith(SpringJUnit4ClassRunner.class) 
public class MyBeanTest { 

    @Autowired 
    private MyBean myBean; // the component under test 

    @Test 
    public void testMyBean() { 
     ... 
    } 
} 

Cela donne un moyen facile de mélanger et faire correspondre les fichiers de configuration réel avec les fichiers de configuration de test. Par exemple, en utilisant Hibernate, je pourrais avoir mon bean sessionFactory dans un fichier de configuration (à utiliser dans les tests et l'application principale), et avoir par bean dataSource dans un autre fichier de configuration (on pourrait utiliser un DriverManagerDataSource à un db en mémoire, l'autre peut utiliser une recherche JNDI).

Mais, certainement tenir compte de @cletus's avertissement ;-)

+0

cela conduirait à beaucoup bean.xml en double pour les tests, pour le test de simulation seulement je préfère proposer easyMock ou similaires cadres fictifs –

+0

Oui - certains de nos tests ont utilisé EasyMock au lieu de cette approche. IMHO EasyMock a tendance à briller pour de vrais tests unitaires. Les tests de type "intégration-like" bénéficient de l'approche ContextConfiguration. – toolkit

16

L'une des raisons pour lesquelles le ressort est décrit comme étant facile à tester est qu'il peut être facile de simplement nouveau ou de simuler le test unitaire.

Sinon, nous avons utilisé la configuration suivante avec un grand succès, et je pense qu'il est assez proche de ce que vous voulez, je fortement le recommande:

Pour tous les haricots qui ont besoin d'implémentations différentes dans des contextes différents, passer au câblage basé sur des annotations. Vous pouvez laisser les autres tels quels.

Mettre en oeuvre l'ensemble suivant des annotations

<context:component-scan base-package="com.foobar"> 
    <context:include-filter type="annotation" expression="com.foobar.annotations.StubRepository"/> 
    <context:include-filter type="annotation" expression="com.foobar.annotations.TestScopedComponent"/> 
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/> 
</context:component-scan> 

Ensuite, vous annoter vos en direct avec les implémentations @Repository, votre stub implémentations avec @StubRepository, tout code qui devrait être présent dans l'appareil de test d'unité UNIQUEMENT avec @TestScopedComponent. Vous pouvez avoir besoin de quelques annotations de plus, mais ce sont un bon début.

Si vous avez beaucoup de spring.xml, vous aurez probablement besoin de faire quelques nouveaux fichiers spring xml qui ne contiennent essentiellement que les définitions de scan de composants. Vous devez normalement ajouter ces fichiers à votre liste @ContextConfiguration habituelle. La raison en est que vous vous retrouvez souvent avec des configurations différentes des analyses de contexte (faites moi confiance, fera au moins 1 annotations de plus si vous faites des tests web, ce qui fait 4 combinaisons pertinentes)

Ensuite, vous utilisez essentiellement le

@ContextConfiguration(locations = { "classpath:/path/to/root-config.xml" }) 
@RunWith(SpringJUnit4ClassRunner.class) 

Notez que cette configuration ne pas vous permettent d'avoir des combinaisons alternatives de données stub/live. Nous avons essayé cela, et je pense que cela a abouti à un désordre que je ne recommanderais à personne;) Nous fournissons soit l'ensemble complet des talons, soit l'ensemble complet des services en direct.

Nous utilisons principalement des dépendances de branchement auto-câblées lorsque nous testons gui près de choses où les dépendances sont généralement assez importantes. Dans les zones plus propres du code, nous utilisons des tests unitaires plus réguliers.

Dans notre système, nous avons les fichiers xml-suivants pour le composant-scan:

  • pour la production web régulière
  • pour le démarrage web avec des talons que
  • pour les tests d'intégration (en JUnit)
  • pour les tests unitaires (en junit)
  • pour les tests web de sélénium (en junit)

Cela signifie que nous avons totalement 5 configurations différentes à l'échelle du système avec lesquelles nous pouvons démarrer l'application. Puisque nous n'utilisons que des annotations, le ressort est assez rapide pour autowire même les tests unitaires que nous voulons câblés. Je sais que c'est non traditionnel, mais c'est vraiment génial.

tests d'intégration exécuté avec une configuration live pleine, et une ou deux fois j'ai décidé de se vraiment pragmatique et que vous voulez avoir un 5 câblages en direct et une seule maquette:

public class HybridTest { 
    @Autowired 
    MyTestSubject myTestSubject; 


    @Test 
    public void testWith5LiveServicesAndOneMock(){ 
    MyServiceLive service = myTestSubject.getMyService(); 
    try { 
      MyService mock = EasyMock.create(...) 
      myTestSubject.setMyService(mock); 

      .. do funky test with lots of live but one mock object 

    } finally { 
      myTestSubject.setMyService(service); 
    } 


    } 
} 

Je sais que le test les puristes vont être sur moi pour ça. Mais parfois, c'est juste une solution très pragmatique qui s'avère très élégante quand l'alternative serait vraiment vraiment moche. Encore une fois, il est généralement dans ces zones gui-near.

+0

Juste curieux, cela ne mène-t-il pas à de l'enfer de maintenance en ce qui concerne @ContextConfiguration individuelle par Testclass? –

+0

Non, nous n'avons que 5 @ConfigurationContxts * à l'échelle du système * dans une grande application. J'ai réédité pour rendre cela plus clair. – krosenvold

+0

Merci pour cette réponse, je l'ai appliqué avec succès. Une chose à noter est que cette façon de procéder est très flexible car vous pouvez utiliser plusieurs balises context-scan: par exemple un par module ou par paquet. En outre, il peut être utile de définir 'use-default-filters = "false". –

0

Je veux faire la même chose, et nous la trouvons essentielle.

Le mécanisme actuel que nous utilisons est assez manuel mais cela fonctionne. Supposons, par exemple, que vous souhaitiez imiter un bean de type Y. Ce que nous faisons, c'est que chaque bean ayant cette dépendance que nous créons implémente une interface - "IHasY". Cette interface est

interface IHasY { 
    public void setY(Y y); 
} 

Ensuite, dans notre test, nous appelons la méthode util ...

public static void insertMock(Y y) { 
     Map invokers = BeanFactory.getInstance().getFactory("core").getBeansOfType(IHasY.class); 
     for (Iterator iterator = invokers.values().iterator(); iterator.hasNext();) { 
      IHasY invoker = (IHasY) iterator.next(); 
      invoker.setY(y); 
     } 
    } 

Je ne veux pas créer un fichier xml tout juste pour injecter cette nouvelle dépendance et qui est la raison pour laquelle je comme ça.

Si vous êtes prêt à créer un fichier de configuration XML, alors la solution serait de créer une nouvelle usine avec les ficelles et de faire de votre usine par défaut un parent de cette usine. Assurez-vous alors que vous chargez tous vos haricots de la nouvelle usine pour enfants. Dans ce cas, la sous-fabrique remplacera les beans de la fabrique parente lorsque les identifiants du bean sont les mêmes.

Maintenant, si, dans mon test, si je pouvais programmer créer une usine, ce serait génial. Avoir à utiliser xml est trop lourd. Je cherche à créer cette usine enfant avec du code. Ensuite, chaque test peut configurer son usine comme il le souhaite. Il n'y a aucune raison pour qu'une telle usine ne fonctionne pas.

6

Voici quelques solutions très complexes et puissantes.

Mais il existe un FAR, FAR plus simple pour accomplir ce que Stas a demandé, ce qui n'implique pas de modifier autre chose qu'une ligne de code dans la méthode de test. Il fonctionne aussi bien pour les tests unitaires que pour les tests d'intégration de Spring, pour les dépendances autowired, les champs privés et protégés.

Ici, il est:

junitx.util.PrivateAccessor.setField(testSubject, "fieldName", mockObject); 
+3

Ou, si vous utilisez spring: 'org.springframework.test.util.ReflectionTestUtils.setField (testSubject," fieldName ", mockObject);' – DrUseful

+2

en supposant que vous avez un accès facile à l'objet qui obtient les dépendances injectées. –

+0

Cela suppose que la classe using n'accède pas à la classe utilisée dans son constructeur ou dans sa méthode init. Dans ce cas, remplacer le champ du haricot par la réflexion arriverait trop tard. –

1

Je n'ai pas les points de réputation à poil sur la réponse de duffymo, mais je voulais juste sonner et dire son était la « bonne réponse » pour moi.

Instaurez un FileSystemXmlApplicationContext dans la configuration de votre test unitaire avec un fichier applicationContext.xml personnalisé. Dans ce fichier XML personnalisé, en haut, faire un comme duffymo indique. Ensuite, déclarez vos simulateurs de fèves, les sources de données non-JNDI, etc., qui remplaceront les identifiants déclarés dans l'importation.

Travaillé comme un rêve pour moi.

7

Voir cette tutorial with @InjectedMock annotation

Il m'a sauvé beaucoup de temps.Vous venez d'utiliser

@Mock 
SomeClass mockedSomeClass 

@InjectMock 
ClassUsingSomeClass service 

@Before 
public void setUp() { 
    MockitoAnnotations.initMocks(this); 
} 

et tous vos problèmes sont résolus. Mockito remplacera l'injection de dépendance de ressort par un faux. Je l'ai juste utilisé moi-même et ça marche très bien.

1

Vous n'avez pas besoin d'utiliser des contextes de test (peu importe est XML ou Java). Depuis Spring Boot 1.4, il y a une nouvelle annotation disponible: @MockBean qui a introduit le support natif pour le moqueur et l'espionnage des haricots de printemps.