helidon
helidon copied to clipboard
Ability to Inject MockBeans in Helidon
Problem
Helidon currently lacks an equivalent to Spring Boot's @MockBean
annotation, which allows for the injection of mock beans into the application context, ensuring that mock dependencies are used when running the application. This feature is essential for improving integration tests in Helidon.
Sample Usecase: Pact (Contract Testing)
One critical use case for this enhancement is contract testing, such as Pact testing, where microservices need to be tested in isolation to ensure their interactions conform to predefined contracts.
Suppose there are two microservices, X and Y, where X calls Y. Consumer tests are created by microservice X, specifying the endpoint, request, and expected response. For such tests to be effective, we need the ability to inject mock dependencies into the running server.
Sample consumer test
{
"consumer": {
"name": "X"
},
"interactions": [
{
"description": "Check if the product exists",
"providerStates": [
{
"name": "Product X010000021 exists"
}
],
"request": {
"body": {
"instance": "v2211",
"pushToURL": "https://XXXXXX.com"
},
"headers": {
"Content-Type": "application/json"
},
"method": "POST",
"path": "/v1/product/check"
},
"response": {
"status": 200
}
}
],
"provider": {
"name": "Y"
}
}
This consumer test is run at the microservice Y, to valid if the request returns the expected response (In the above example POST to /v1/product/check with the provided request should return a status 200)
Spring Boot Example
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Provider("Y")
public class PricingServiceProviderPactTest {
@MockBean
private ProductClient productClient; // Replaces the bean with a mock in the application context
@LocalServerPort
private int port;
@BeforeEach
void before(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", port, "/"));
}
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context, HttpRequest request) {
context.verifyInteraction();
}
@State("Product X010000021 exists")
public void setupProductX010000021() {
when(productClient.fetch((Set<String>) argThat(contains("X010000021")), any())).thenReturn(product);
}
}
In Spring Boot, the server starts, and the ProductClient is injected with a mock. When Pact runs the consumer tests, it calls the REST endpoint of the server, and since we can mock the injections, we can return the required data without actually performing the operation.
Helidon Example
In contrast, in Helidon, for a running server, you cannot inject a MockBean. Therefore, the only way to test the Pact is to create a new server with a mocked endpoint class. However, this approach does not constitute real integration testing, as the actual REST endpoint is not called. Consequently, if any changes occur in the server's path, the consumer test may not break, as the endpoint is manually specified in the test router.
SampleProviderTest
@HelidonTest
@Provider("Y")
@DisableDiscovery
@AddExtensions({
@AddExtension(ServerCdiExtension.class),
@AddExtension(JaxRsCdiExtension.class),
@AddExtension(CdiComponentProvider.class)
})
public class SampleProviderTest implements SampleEndpointMocked {
@Inject
WebTarget webTarget;
@Mock
ProductClient productClient;
@BeforeEach
void before(PactVerificationContext context) {
String host = webTarget.getUri().getHost();
int port = webTarget.getUri().getPort();
context.setTarget(new HttpTestTarget(host, port, "/"));
}
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context, HttpRequest request) {
context.verifyInteraction();
}
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
}
@State("Product X010000021 exists")
public void setupProductX010000021() {
when(productClient.fetch((Set<String>) argThat(contains("X010000021")), any())).thenReturn(product);
}
@Override
public ProductClient productClient() {
return productClient;
}
}
SampleEndpointMocked
@Path("/v1/product/check")
@ApplicationScoped
public interface SampleEndpointMocked {
ProductClient productClient();
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
default Response sampleEndPoint(@HeaderParam("Authorization") String authHeaderValue, String inputJson) {
ApplicationEndpoint applicationEndpoint = new ApplicationEndpoint(productClient());
return applicationEndpoint.actualEndpoint(authHeaderValue, inputJson);
}
}