web-dev-qa-db-fra.com

Comment écrire des tests d'intégration avec spring-cloud-netflix et feign

J'utilise Spring-Cloud-Netflix pour la communication entre les micro-services. Supposons que j'ai deux services, Foo et Bar, et que Foo utilise l'un des points d'extrémité REST de Bar. J'utilise une interface annotée avec @FeignClient:

@FeignClient
public interface BarClient {
  @RequestMapping(value = "/some/url", method = "POST")
  void bazzle(@RequestBody BazzleRequest);
}

Ensuite, j'ai une classe de service SomeService dans Foo, qui appelle la BarClient.

@Component
public class SomeService {
    @Autowired
    BarClient barClient;

    public String doSomething() {
      try {
        barClient.bazzle(new BazzleRequest(...));
        return "so bazzle my eyes dazzle";
      } catch(FeignException e) {
        return "Not bazzle today!";
      }

    }
}

Maintenant, pour m'assurer que la communication entre les services fonctionne, je veux créer un test qui déclenche une vraie requête HTTP contre un faux serveur Bar, en utilisant quelque chose comme WireMock. Le test doit s'assurer que feign décode correctement la réponse du service et la renvoie à SomeService.

public class SomeServiceIntegrationTest {

    @Autowired SomeService someService;

    @Test
    public void shouldSucceed() {
      stubFor(get(urlEqualTo("/some/url"))
        .willReturn(aResponse()
            .withStatus(204);

      String result = someService.doSomething();

      assertThat(result, is("so bazzle my eyes dazzle"));
    }

    @Test
    public void shouldFail() {
      stubFor(get(urlEqualTo("/some/url"))
        .willReturn(aResponse()
            .withStatus(404);

      String result = someService.doSomething();

      assertThat(result, is("Not bazzle today!"));
    }
}

Comment puis-je injecter un tel serveur WireMock dans eureka, de sorte que feign soit capable de le trouver et de communiquer avec lui? De quel type de magie d'annotation ai-je besoin?

16
Bastian Voigt

Voici un exemple d'utilisation de WireMock pour tester la configuration SpringBoot avec le client Feign et le repli Hystrix.

Si vous utilisez Eureka en tant que découverte de serveur, vous devez le désactiver en définissant une propriété "eureka.client.enabled=false".

Premièrement, nous devons activer la configuration Feign/Hystrix pour notre application: 

@SpringBootApplication
@EnableFeignClients
@EnableCircuitBreaker
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@FeignClient(
        name = "bookstore-server",
        fallback = BookClientFallback.class,
        qualifier = "bookClient"
)
public interface BookClient {

    @RequestMapping(method = RequestMethod.GET, path = "/book/{id}")
    Book findById(@PathVariable("id") String id);
}

@Component
public class BookClientFallback implements BookClient {

    @Override
    public Book findById(String id) {
        return Book.builder().id("fallback-id").title("default").isbn("default").build();
    }
}

Veuillez noter que nous spécifions une classe de secours pour le client Feign. La classe de secours sera appelée chaque fois que l'appel du client Feign échoue (par exemple, délai de connexion).

Pour que les tests fonctionnent, nous devons configurer le loadbalancer du ruban (utilisé en interne par le client Feign lors de l'envoi de la requête http):

@RunWith(SpringRunner.class)
@SpringBootTest(properties = {
        "feign.hystrix.enabled=true"
})
@ContextConfiguration(classes = {BookClientTest.LocalRibbonClientConfiguration.class})
public class BookClientTest {

    @Autowired
    public BookClient bookClient;

    @ClassRule
    public static WireMockClassRule wiremock = new WireMockClassRule(
            wireMockConfig().dynamicPort()));

    @Before
    public void setup() throws IOException {
        stubFor(get(urlEqualTo("/book/12345"))
                .willReturn(aResponse()
                        .withStatus(HttpStatus.OK.value())
                        .withHeader("Content-Type", MediaType.APPLICATION_JSON)
                        .withBody(StreamUtils.copyToString(getClass().getClassLoader().getResourceAsStream("fixtures/book.json"), Charset.defaultCharset()))));
    }

    @Test
    public void testFindById() {
        Book result = bookClient.findById("12345");

        assertNotNull("should not be null", result);
        assertThat(result.getId(), is("12345"));
    }

    @Test
    public void testFindByIdFallback() {
        stubFor(get(urlEqualTo("/book/12345"))
                .willReturn(aResponse().withFixedDelay(60000)));

        Book result = bookClient.findById("12345");

        assertNotNull("should not be null", result);
        assertThat(result.getId(), is("fallback-id"));
    }

    @TestConfiguration
    public static class LocalRibbonClientConfiguration {
        @Bean
        public ServerList<Server> ribbonServerList() {
            return new StaticServerList<>(new Server("localhost", wiremock.port()));
        }
    }
}

La liste de serveurs de ruban doit correspondre à l'URL (hôte et port) de notre configuration WireMock.

11
mladzo

Voici un exemple de câblage de Feign et WireMock avec un port aléatoire (basé sur Spring-Boot github answer). 

@RunWith(SpringRunner.class)
@SpringBootTest(properties = "google.url=http://google.com") // emulate application.properties
@ContextConfiguration(initializers = PortTest.RandomPortInitializer.class)
@EnableFeignClients(clients = PortTest.Google.class)
public class PortTest {

    @ClassRule
    public static WireMockClassRule wireMockRule = new WireMockClassRule(
        wireMockConfig().dynamicPort()
    );

    @FeignClient(name = "google", url = "${google.url}")
    public interface Google {    
        @RequestMapping(method = RequestMethod.GET, value = "/")
        String request();
    }

    @Autowired
    public Google google;

    @Test
    public void testName() throws Exception {
        stubFor(get(urlEqualTo("/"))
                .willReturn(aResponse()
                        .withStatus(HttpStatus.OK.value())
                        .withBody("Hello")));

        assertEquals("Hello", google.request());
    }


    public static class RandomPortInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
        @Override
        public void initialize(ConfigurableApplicationContext applicationContext) {

            // If the next statement is commented out, 
            // Feign will go to google.com instead of localhost
            TestPropertySourceUtils
                .addInlinedPropertiesToEnvironment(applicationContext,
                    "google.url=" + "http://localhost:" + wireMockRule.port()
            );
        }
    }
}

Alternativement vous pouvez essayer de jouer avec System.setProperty() dans la méthode @BeforeClass de votre test.

5
Alexander

Auparavant, il y avait deux options pour effectuer des tests d'intégration pour les applications microservices:

  1. Déploiement de services dans un environnement de test et réalisation de bout en bout Tests 
  2. Se moquer des autres microservices

La première option présente l’inconvénient évident de devoir déployer toutes les dépendances (autres services, bases de données, etc.). De plus, il est lent et difficile à déboguer.

La deuxième option est plus rapide et moins compliquée, mais il est facile de se retrouver avec des talons qui se comportent différemment de la réalité dans le temps, en raison de possibles changements de code. Il est donc possible d'avoir des tests réussis mais une application qui échoue lorsqu'elle est déployée pour prod.

Une meilleure solution consisterait à utiliser la vérification de contrat basée sur le client, afin de vous assurer que l'API du service fournisseur est conforme aux appels du consommateur. À cette fin, les développeurs Spring peuvent utiliser Spring Cloud Contract . Pour les autres environnements, il existe un cadre appelé PACT . Les deux peuvent également être utilisés avec des clients Feign. Ici est un exemple avec PACT.

1
humbaba

Il n’est probablement pas possible de faire communiquer directement WireMock avec Eureka Server, mais vous pouvez utiliser d’autres variantes pour configurer l’environnement de test dont vous avez besoin.

  1. Dans l'environnement de test, vous pouvez déployer Eureka Service Registry dans un conteneur de servlets Jetty autonome et toutes les annotations fonctionneront comme dans un environnement de production réel. 
  2. Si vous ne souhaitez pas utiliser la logique de noeud final BarClient réelle et que le test d'intégration concerne uniquement la couche de transport http réelle, vous pouvez utiliser Mockito pour le stub de noeud final BarClient.

Je suppose que pour implémenter 1 et 2 en utilisant Spring-Boot, vous devrez créer deux applications distinctes pour un environnement de test. Un pour Eureka Service Registry sous Jetty et un autre pour BarClient stub final sous Jetty également. 

Une autre solution consiste à configurer manuellement Jetty et Eureka dans un contexte d'application test. Je pense que c'est un meilleur moyen, mais dans ce cas, vous devez comprendre ce que les annotations @EnableEurekaServer et @EnableDiscoveryClient font avec le contexte d'application Spring.

0
Sergey Bespalov