Miguel Delgado
12/26/2024, 12:35 AM@Test
void shouldThrowExceptionWhenAuthorizationFails() throws Exception {
Mockito.when(authorizationClient.execute())
.thenReturn(new Authorize("fail", new Data(false)));
var response = testScenario.executeTransferMoneyRequest(
AMOUNT_125, 3, 38
);
assertThat(response.getStatus()).isEqualTo(403);
Mockito.verify(authorizationClient, Mockito.times(1)).execute();
}
// I will use WireMock here.
public void paymentAllowedByAuthorizer(Boolean value) {
if (Boolean.FALSE.equals(value)) {
Mockito.when(authorizationClient.execute())
.thenReturn(new Authorize("fail", new Data(value)));
} else {
Mockito.when(authorizationClient.execute())
.thenReturn(new Authorize("success", new Data(value)));
}
}
I'm running WireMock via docker-compose
, along with my database.
services:
wiremock:
image: "wiremock/wiremock:latest"
container_name: wiremock-standalone
entrypoint: [ "/docker-entrypoint.sh", "--global-response-templating", "--disable-gzip", "--verbose" ]
ports:
- "8080:8080"
postgres:
image: postgres:15
container_name: simplifypay_postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: miguel
POSTGRES_DB:
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
wiremock_data:
I defined a Bean for WireMock in my test configuration class.
@TestConfiguration
public class TestConfig {
@Bean
public TransferMoneyTestScenario transferMoneyTestScenario() {
return new TransferMoneyTestScenario();
}
@Bean
public UserTestScenario userTestScenario() {
return new UserTestScenario();
}
@Bean
public WireMockServer wireMockServer() {
return new WireMockServer(8080);
}
}
My external server is a devtools instance, and it only returns two types of responses:
{
"status": "success",
"data": {
"authorization": true
}
}
OR
{
"status": "fail",
"data": {
"authorization": false
}
}
This is the class where I'll abstract the code to mock my external authorization server.
@SpringBootTest
public class MockingAuth {
@Autowired
private WireMockServer wireMockServer;
@Autowired
private JacksonTester<Authorize> jacksonTester;
@BeforeEach
void init() {
WireMock.configureFor("localhost", 8080);
WireMock.startRecording("<<https://util.devi.tools/api/v2/authorize>>");
}
@Test
void recordingExternalRequests() throws IOException {
var response = new Authorize("success", new Data(true));
var json = jacksonTester.write(response).getJson();
WireMock.stubFor(post(urlPathEqualTo("/api/v2/authorize"))
.willReturn(aResponse()
.withStatus(200)
.withBody(json)));
System.out.println("Make external service calls here to record.");
}
@AfterEach
void tearDown() {
var toReturn = WireMock.stopRecording().getStubMappings();
System.out.println("Recording finished. Stubs generated: " + toReturn.size());
wireMockServer.stop();
}
}
However, I'm getting this error when running the test:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'SimplifyPay.infrastructure.controllers.ClasseDeTeste': Unsatisfied dependency expressed through field 'wireMockServer': No qualifying bean of type 'com.github.tomakehurst.wiremock.WireMockServer' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)
Lee Turner
12/26/2024, 11:45 AMClasseDeTeste
class. Is that class in the code you added to your question ?Miguel Delgado
12/26/2024, 9:07 PM@Service
@RequiredArgsConstructor(onConstructor= @__(@Autowired))
public class TransferMoneyImpl implements TransferMoneyUseCase {
// ... (Logger, Repositories, etc.)
private final AuthorizationClient authClient;
@Override
@Transactional
public TransferMoneyResponse execute(TransferMoneyRequest req) {
// ... (Validations)
authClient.execute(); // Calling the external authorization service
<http://logger.info|logger.info>("External service authorized");
// ... (Rest of the transfer logic)
}
// ... (Other methods)
}
The AuthorizationClient
implementation is:
@FeignClient(
value = "authorization-dev-tools",
url="<<https://util.devi.tools/api/v2>>"
)
public interface AuthorizationClient {
@GetMapping("/authorize")
Authorize execute();
}
In my integration tests, I validate different use cases, and one of these tests checks how the API reacts when the external authorization denies the transfer.
For this, I want to mock my AuthorizationClient
, but I haven't succeeded in doing so yet.
Currently, I'm trying to mock the AuthorizationClient
using WireMock. I configured my pom.xml
and docker-compose
for this:
<!-- WireMock for mocking external APIs -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
services:
wiremock:
image: "wiremock/wiremock:latest"
container_name: wiremock-standalone
entrypoint: [ "/docker-entrypoint.sh", "--global-response-templating", "--disable-gzip", "--verbose" ]
ports:
- "8080:8080"
I added WireMockServer
to my test configuration class as a Bean:
@TestConfiguration
public class TestConfig {
@Bean
public WireMockServer wireMockServer() {
return new WireMockServer(8080);
}
}
I created an abstraction for the AuthorizationClient
test:
public class AuthorizationClientTest {
@Autowired
private static JacksonTester<Authorize> authorizeJacksonTester;
public static void stubAuthorizationSuccess() throws Exception {
var json = authorizeJacksonTester.write(new Authorize("success", new Data(true))).getJson();
stubFor(get(urlEqualTo("/authorize"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody(json)));
}
public static void stubAuthorizationForbidden() throws Exception {
var json = authorizeJacksonTester.write(new Authorize("fail", new Data(false))).getJson();
stubFor(get(urlEqualTo("/authorize"))
.willReturn(aResponse()
.withStatus(403)
.withHeader("Content-Type", "application/json")
.withBody(json)));
}
}
And I implemented this abstraction in my test class:
@SpringBootTest()
@AutoConfigureWireMock(port = 8080)
public class MockingAuth {
@Autowired
private AuthorizationClient authorizationClient;
@Test
public void shouldReturnSuccessResponse() throws Exception {
stubAuthorizationSuccess();
Authorize response = authorizationClient.execute();
assertThat(response.status()).isEqualTo("success");
assertThat(response.data().authorization()).isTrue();
}
@Test
public void shouldReturnForbiddenResponse() throws Exception {
stubAuthorizationForbidden();
Authorize response = authorizationClient.execute();
assertThat(response.status()).isEqualTo("fail");
assertThat(response.data().authorization()).isFalse();
}
}
The docker-compose
setup works perfectly, and I can access WireMock routes on localhost:8080
.
However, when I run my test, I get the following error with a massive stack trace, and I haven't been able to figure out the solution >:
the stacktrace summary
java.lang.IllegalStateException: Failed to load ApplicationContext for [WebMergedContextConfiguration...]
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.cloud.contract.wiremock.WireMockConfiguration': Invocation of init method failed
Caused by: com.github.tomakehurst.wiremock.common.FatalStartupException: java.lang.RuntimeException: java.io.IOException: Failed to bind to /0.0.0.0:8080
Caused by: java.lang.RuntimeException: java.io.IOException: Failed to bind to /0.0.0.0:8080
Caused by: java.io.IOException: Failed to bind to /0.0.0.0:8080
Caused by: java.net.BindException: Address already in use: bind
Miguel Delgado
12/26/2024, 9:10 PMLee Turner
12/26/2024, 9:26 PMspring-cloud-starter-contract-stub-runner
uses a really old version of WireMock. We now have an official Spring integration that would probably be worth checking out given you are using the 3.x.x
release of Spring Boot.
From looking at your error, WireMock is trying to connect to localhost:8080 but can't because something is already running on that port. Could it be that spring-cloud-starter-contract-stub-runner
is trying to spin up a WireMock instance on localhost:8080 but you already have WireMock running in docker on that port ?Lee Turner
12/26/2024, 9:27 PMLee Turner
12/26/2024, 9:32 PMMiguel Delgado
12/26/2024, 10:29 PMMiguel Delgado
12/27/2024, 1:18 AM@EnableWireMock({@ConfigureWireMock(name = "auth_mock", port = 9000)})
public class MockingAuth {
@Value("${wiremock.server.baseUrl}")
private String wireMockUrl;
public static void stubAuthorizationSuccess() throws Exception {
System.out.println("Stubbing /authorize for success");
stubFor(get(urlEqualTo("/authorize"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\\\\"status\\\\":\\\\"success\\\\",\\\\"data\\\\":{\\\\"authorized\\\\":true}}")));
}
public static void stubAuthorizationForbidden() throws Exception {
System.out.println("Stubbing /authorize for forbidden");
stubFor(get(urlEqualTo("/authorize"))
.willReturn(aResponse()
.withStatus(403)
.withHeader("Content-Type", "application/json")
.withBody("{\\\\"status\\\\":\\\\"fail\\\\",\\\\"data\\\\":{\\\\"authorized\\\\":false}}")));
}
}
My ControllerTest
@SpringBootTest
@AutoConfigureJsonTesters
@AutoConfigureMockMvc
@Transactional
@ActiveProfiles("test")
@EnableWireMock({@ConfigureWireMock(name = "auth_mock", port = 9000)})
@Import(TestConfig.class)
class ControllerTest {
// Other methods...
@Test
void shouldThrowExceptionWhenAuthorizationFails() throws Exception {
// Given
var commonUserId = userTestScenario.getIdFromResponse(commonUserResponse);
var merchantUserId = userTestScenario.getIdFromResponse(merchantUserResponse);
// Setup initial balances
stubAuthorizationForbidden();
// When
var response = testScenario.executeTransferMoneyRequest(
AMOUNT_100, commonUserId, merchantUserId
);
// Then
assertThat(response.getStatus()).isEqualTo(403);
}
@Test
void shouldTransferMoneyWhenAuthorizationWorks() throws Exception {
// Given
var commonUserId = userTestScenario.getIdFromResponse(commonUserResponse);
var merchantUserId = userTestScenario.getIdFromResponse(merchantUserResponse);
// Setup initial balances
stubAuthorizationSuccess();
// When
var response = testScenario.executeTransferMoneyRequest(
AMOUNT_100, commonUserId, merchantUserId
);
// Then
assertThat(response.getStatus()).isEqualTo(200);
}
// Other methods...
}
Feign Client
I configured the base_url
to be environment-specific, allowing for separate configurations for production and test environments:
@FeignClient(
value = "authorization-dev-tools",
url="${authorization-client.base-url}"
)
public interface AuthorizationClient {
@GetMapping("/authorize")
Authorize execute();
}
Production-Profile
authorization-client.base-url=<https://util.devi.tools/api/v2>
Test-Profile
authorization-client:
base-url: <<http://localhost:9000>>
Once again, thank you for your quick responses and support! This made a huge difference in helping me overcome this challenge. 🙏Miguel Delgado
12/27/2024, 1:22 AMLee Turner
12/27/2024, 7:55 AM