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?
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.
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.
Auparavant, il y avait deux options pour effectuer des tests d'intégration pour les applications 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.
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.
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.