我将
spring-boot
与 WebClient
一起使用,它作为 bean 自动装配。
问题:在编写
junit
集成测试时,我必须使用okhttpMockWebServer
。该模拟始终在随机端口上启动,例如 localhost:14321
。
现在我的
WebClient
当然有一个固定的 url 来发送请求。该 url 可能由 application.properties
参数(如 webclient.url=https://my.domain.com/
)给出,因此我可以在 junit
测试中覆盖该字段。但只是静态的。
问题:如何重置
WebClient
中的 @SpringBootTest
bean,以便它始终将请求发送到我的模拟服务器?
@Service
public class WebClientService {
public WebClientService(WebClient.Builder builder, @Value("${webclient.url}" String url) {
this.webClient = builder.baseUrl(url)...build();
}
public Response send() {
return webClient.body().exchange().bodyToMono();
}
}
@Service
public void CallingService {
@Autowired
private WebClientService service;
public void call() {
service.send();
}
}
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MyWebTest {
@Autowired
private CallingService calling;
@Test
public void test() {
MockWebServer mockWebServer = new MockWebServer();
System.out.println("Current mock server url: " + mockWebServer.url("/").toString()); //this is random
mockWebServer.enqueue(new MockResponse()....);
//TODO how to make the mocked server url public to the WebClient?
calling.call();
}
}
如您所见,我正在编写完整的现实世界 junit 集成测试。唯一的问题是:如何将
MockWebServer
url 和端口传递到 WebClient
,以便它自动将请求发送到我的模拟??
旁注:我绝对需要这里的
MockWebServer
随机端口,以免干扰其他正在运行的测试或应用程序。因此必须坚持使用随机端口,并找到一种将其传递到 Web 客户端的方法(或动态覆盖应用程序属性)。
更新: 我想出了以下有效的方法。但也许有人知道如何使模拟服务器字段非静态?
@ContextConfiguration(initializers = RandomPortInitializer.class)
public abstract class AbstractITest {
@ClassRule
public static final MockWebServer mockWebServer = new MockWebServer();
public static class RandomPortInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(applicationContext,
"webclient.url=" + mockWebServer.url("/").toString());
}
}
}
从 Spring Framework 5.2.5 (Spring Boot 2.x) 开始,您可以使用
DynamicPropertySource
注释,这非常方便。
这是一个完整的示例,如何将它与
MockWebServer
一起使用来绑定正确的端口:
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public abstract class AbstractIT {
static MockWebServer mockWebServer;
@DynamicPropertySource
static void properties(DynamicPropertyRegistry r) throws IOException {
r.add("some-service.url", () -> "http://localhost:" + mockWebServer.getPort());
}
@BeforeAll
static void beforeAll() throws IOException {
mockWebServer = new MockWebServer();
mockWebServer.start();
}
@AfterAll
static void afterAll() throws IOException {
mockWebServer.shutdown();
}
}
这是您如何使用 MockWebServer 的 URL 设置 WebClient 基本 URL。
WebClient 和 MockWebServer 足够轻,可以为每个测试重新创建。
@SpringBootTest
class Tests {
@Autowired
private WebClient.Builder webClientBuilder;
private MockWebServer mockWebServer;
private WebClient client;
@BeforeEach
void setUp() throws Exception {
mockWebServer = new MockWebServer();
mockWebServer.start();
// Set WebClinet base URL with the the mock web server URL
webClientBuilder.baseUrl(mockWebServer.url("").toString());
client = webClientBuilder.build();
}
@AfterEach
void tearDown() throws Exception {
mockWebServer.shutdown();
}
@Test
void bodyIsExpected() throws InterruptedException {
mockWebServer.enqueue(new MockResponse().setStatus("HTTP/1.1 200 OK").setBody("Hello World!")
.addHeader("Content-Type", "text/plain"));
ResponseEntity<String> response = client.get().retrieve().toEntity(String.class).block();
assertThat(response.getBody(), is("Hello World!"));
}
}
这是我正在使用的解决方案。使用 SpringBootTest 的 Spring Boot 2 和 Spring-Webflux 5.3.30
MyServiceClientTestConfig 创建注入的 beans 端口和 myServiceClient,其中
如文档所述,我们不应该在测试之间重复使用 MockWebServer 实例,因此在每次测试之前使用我们在测试初始化时发现的空闲端口创建它,并在每次测试后将其关闭。
我想有一个很小的机会,因为我在 Spring 初始化应用程序上下文时找到了该端口,并且在第一次调用 init 之前和测试之间有很短的一段时间内 MockWebServer 没有使用该端口,所以另一个进程可能会占用端口,但在测试生命周期内发生这种情况的可能性似乎非常小。
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class MyServiceClientTest {
private static final Logger LOG = LoggerFactory.getLogger(MyServiceClientTest.class);
private MockWebServer mockWebServer;
@Autowired
private MyServiceClient myServiceClient;
@Autowired
private Integer port;
@BeforeEach
void init() throws IOException {
assertNotNull(myServiceClient);
assertNotNull(port);
// MockWebServer not re-usable between tests
mockWebServer = new MockWebServer();
mockWebServer.start(port);
assertNotNull(mockWebServer);
assertEquals(port, mockWebServer.getPort());
}
@AfterEach
void cleanUp() throws IOException {
// MockWebServer not re-usable between tests
mockWebServer.shutdown();
}
@Test
public void placeholderTest1() {
LOG.info("Hello 1 {}", mockWebServer.getPort());
}
@Test
public void placeholderTest2() {
LOG.info("Hello 2 {}", mockWebServer.getPort());
}
@Configuration
static class MyServiceClientTestConfig {
@Bean
Integer getFreePort () {
// find a free port to use for this test
return TestSocketUtils.findAvailableTcpPort();
}
@Bean
MyServiceClient getMyServiceClient(Integer port) {
return new MyServiceClient(String.format("http://localhost:%d", port));
}
}
}