如何对 spring 的网关进行单元测试?
How to unit test spring's gateway?
我的网关会将流量重定向到许多不同的服务(在不同的域名下)。我如何测试网关的配置?只有一项服务,我可以设置模拟服务器(如 httpbin)并测试响应。对于多项服务,我宁愿避免启动整个 docker 网络或更改 locak dns 别名。 spring 是否提供任何测试网关的轻量级方法?
以下是如何使用 API Simulator 实现您想要的:
package my.package;
import static com.apisimulator.embedded.SuchThat.isEqualTo;
import static com.apisimulator.embedded.SuchThat.startsWith;
import static com.apisimulator.embedded.http.HttpApiSimulation.httpApiSimulation;
import static com.apisimulator.embedded.http.HttpApiSimulation.httpRequest;
import static com.apisimulator.embedded.http.HttpApiSimulation.httpResponse;
import static com.apisimulator.embedded.http.HttpApiSimulation.simlet;
import static com.apisimulator.http.Http1Header.CONTENT_TYPE;
import static com.apisimulator.http.HttpMethod.CONNECT;
import static com.apisimulator.http.HttpMethod.GET;
import static com.apisimulator.http.HttpStatus.OK;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import java.time.Duration;
import java.util.Map;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.SocketUtils;
import com.apisimulator.embedded.http.JUnitHttpApiSimulation;
@RunWith(SpringRunner.class)
@SpringBootTest(
webEnvironment = RANDOM_PORT,
properties = {
"management.server.port=${test.port}", "logging.level.root=info",
// Configure the Gateway to use HTTP proxy - the API Simulator
// instance running at localhost:6090
"spring.cloud.gateway.httpclient.proxy.host=localhost",
"spring.cloud.gateway.httpclient.proxy.port=6090"
//"logging.level.reactor.netty.http.server=debug",
//"spring.cloud.gateway.httpserver.wiretap=true"
}
)
@Import(ServiceGatewayApplication.class)
public class ServiceGatewayApplicationTest
{
// Configure an API simulation. This starts up an instance
// of API Simulator on localhost, default port 6090
@ClassRule
public static final JUnitHttpApiSimulation clApiSimulation = JUnitHttpApiSimulation
.as(httpApiSimulation("svc-gateway-backends"));
protected static int managementPort;
@LocalServerPort
protected int port = 0;
protected String baseUri;
protected WebTestClient webClient;
@BeforeClass
public static void beforeClass()
{
managementPort = SocketUtils.findAvailableTcpPort();
System.setProperty("test.port", String.valueOf(managementPort));
// Configure simlets for the API simulation
// @formatter:off
clApiSimulation.add(simlet("http-proxy")
.when(httpRequest(CONNECT))
.then(httpResponse(OK))
);
clApiSimulation.add(simlet("test-domain-1")
.when(httpRequest()
.whereMethod(GET)
.whereUriPath(isEqualTo("/static"))
// The `host` header is used to determine the actual destination
.whereHeader("host", startsWith("domain-1.com"))
)
.then(httpResponse()
.withStatus(OK)
.withHeader(CONTENT_TYPE, "application/text")
.withBody("{ \"domain\": \"1\" }")
)
);
clApiSimulation.add(simlet("test-domain-2")
.when(httpRequest()
.whereMethod(GET)
.whereUriPath(isEqualTo("/v1/api/foo"))
.whereHeader("host", startsWith("domain-2.com"))
)
.then(httpResponse()
.withStatus(OK)
.withHeader(CONTENT_TYPE, "application/json; charset=UTF-8")
.withBody(
"{\n" +
" \"domain\": \"2\"\n" +
"}"
)
)
);
// @formatter:on
}
@AfterClass
public static void afterClass()
{
System.clearProperty("test.port");
}
@Before
public void setup()
{
// @formatter:off
baseUri = "http://localhost:" + port;
webClient = WebTestClient.bindToServer()
.baseUrl(baseUri)
.responseTimeout(Duration.ofSeconds(2))
.build();
// @formatter:on
}
@Test
public void test_domain1()
{
// @formatter:off
webClient.get()
.uri("/static")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).consumeWith(result ->
assertThat(result.getResponseBody()).isEqualTo("{ \"domain\": \"1\" }")
);
// @formatter:on
}
@Test
public void test_domain2()
{
// @formatter:off
webClient.get()
.uri("/v1/api/foo")
.exchange()
.expectStatus().isOk()
.expectHeader()
.contentType("application/json; charset=UTF-8")
.expectBody(Map.class).consumeWith(result ->
assertThat(result.getResponseBody()).containsEntry("domain", "2")
);
// @formatter:on
}
}
大部分代码基于 GatewaySampleApplicationTests class 来自 Spring Cloud Gateway 项目。
以上假定网关具有与这些类似的路由(仅限片段):
...
uri: "http://domain-1.com"
predicates:
- Path=/static
...
uri: "http://domain-2.com"
predicates:
- Path=/v1/api/foo
...
@apsisim 提供了使用网络代理的好主意。但他建议的工具不在任何 Maven 仓库中,并且具有商业许可。什么对我有用:
运行 网关,因此它将使用代理(你可以更花哨,找到一个空闲端口):
private const val proxyPort = 1080
@SpringBootTest(
properties = [
//"logging.level.reactor.netty.http.server=debug",
//"spring.cloud.gateway.httpserver.wiretap=true",
//"spring.cloud.gateway.httpclient.wiretap=true",
"spring.cloud.gateway.httpclient.proxy.host=localhost",
"spring.cloud.gateway.httpclient.proxy.port=$proxyPort"
]
)
然后使用模拟网络服务器作为代理
testImplementation("com.squareup.okhttp3:mockwebserver:4.2.1")
testImplementation("com.squareup.okhttp3:okhttp:4.2.1")
然后您的所有请求都将转到您的代理。请记住,http 协议指定对新服务器的第一个请求需要通过代理进行隧道传输,因此当您对网关进行第一个请求时,网关将向代理发送 2 个请求:
testClient.get()
.uri(path)
.header("host", gatewayDns)
.exchange()
nextRequestFromGateway {
method `should equal` "CONNECT"
headers[HOST] `should equal` "$realUrlBehindGateway:80"
}
nextRequestFromGateway {
path `should equal` "/api/v1/whatever"
headers[HOST] `should equal` realUrlBehindGateway
}
...
fun nextRequestFromGateway(block : RecordedRequest.() -> Unit) {
mockWebServer.takeRequest().apply (block)
}
我的网关会将流量重定向到许多不同的服务(在不同的域名下)。我如何测试网关的配置?只有一项服务,我可以设置模拟服务器(如 httpbin)并测试响应。对于多项服务,我宁愿避免启动整个 docker 网络或更改 locak dns 别名。 spring 是否提供任何测试网关的轻量级方法?
以下是如何使用 API Simulator 实现您想要的:
package my.package;
import static com.apisimulator.embedded.SuchThat.isEqualTo;
import static com.apisimulator.embedded.SuchThat.startsWith;
import static com.apisimulator.embedded.http.HttpApiSimulation.httpApiSimulation;
import static com.apisimulator.embedded.http.HttpApiSimulation.httpRequest;
import static com.apisimulator.embedded.http.HttpApiSimulation.httpResponse;
import static com.apisimulator.embedded.http.HttpApiSimulation.simlet;
import static com.apisimulator.http.Http1Header.CONTENT_TYPE;
import static com.apisimulator.http.HttpMethod.CONNECT;
import static com.apisimulator.http.HttpMethod.GET;
import static com.apisimulator.http.HttpStatus.OK;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import java.time.Duration;
import java.util.Map;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.SocketUtils;
import com.apisimulator.embedded.http.JUnitHttpApiSimulation;
@RunWith(SpringRunner.class)
@SpringBootTest(
webEnvironment = RANDOM_PORT,
properties = {
"management.server.port=${test.port}", "logging.level.root=info",
// Configure the Gateway to use HTTP proxy - the API Simulator
// instance running at localhost:6090
"spring.cloud.gateway.httpclient.proxy.host=localhost",
"spring.cloud.gateway.httpclient.proxy.port=6090"
//"logging.level.reactor.netty.http.server=debug",
//"spring.cloud.gateway.httpserver.wiretap=true"
}
)
@Import(ServiceGatewayApplication.class)
public class ServiceGatewayApplicationTest
{
// Configure an API simulation. This starts up an instance
// of API Simulator on localhost, default port 6090
@ClassRule
public static final JUnitHttpApiSimulation clApiSimulation = JUnitHttpApiSimulation
.as(httpApiSimulation("svc-gateway-backends"));
protected static int managementPort;
@LocalServerPort
protected int port = 0;
protected String baseUri;
protected WebTestClient webClient;
@BeforeClass
public static void beforeClass()
{
managementPort = SocketUtils.findAvailableTcpPort();
System.setProperty("test.port", String.valueOf(managementPort));
// Configure simlets for the API simulation
// @formatter:off
clApiSimulation.add(simlet("http-proxy")
.when(httpRequest(CONNECT))
.then(httpResponse(OK))
);
clApiSimulation.add(simlet("test-domain-1")
.when(httpRequest()
.whereMethod(GET)
.whereUriPath(isEqualTo("/static"))
// The `host` header is used to determine the actual destination
.whereHeader("host", startsWith("domain-1.com"))
)
.then(httpResponse()
.withStatus(OK)
.withHeader(CONTENT_TYPE, "application/text")
.withBody("{ \"domain\": \"1\" }")
)
);
clApiSimulation.add(simlet("test-domain-2")
.when(httpRequest()
.whereMethod(GET)
.whereUriPath(isEqualTo("/v1/api/foo"))
.whereHeader("host", startsWith("domain-2.com"))
)
.then(httpResponse()
.withStatus(OK)
.withHeader(CONTENT_TYPE, "application/json; charset=UTF-8")
.withBody(
"{\n" +
" \"domain\": \"2\"\n" +
"}"
)
)
);
// @formatter:on
}
@AfterClass
public static void afterClass()
{
System.clearProperty("test.port");
}
@Before
public void setup()
{
// @formatter:off
baseUri = "http://localhost:" + port;
webClient = WebTestClient.bindToServer()
.baseUrl(baseUri)
.responseTimeout(Duration.ofSeconds(2))
.build();
// @formatter:on
}
@Test
public void test_domain1()
{
// @formatter:off
webClient.get()
.uri("/static")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).consumeWith(result ->
assertThat(result.getResponseBody()).isEqualTo("{ \"domain\": \"1\" }")
);
// @formatter:on
}
@Test
public void test_domain2()
{
// @formatter:off
webClient.get()
.uri("/v1/api/foo")
.exchange()
.expectStatus().isOk()
.expectHeader()
.contentType("application/json; charset=UTF-8")
.expectBody(Map.class).consumeWith(result ->
assertThat(result.getResponseBody()).containsEntry("domain", "2")
);
// @formatter:on
}
}
大部分代码基于 GatewaySampleApplicationTests class 来自 Spring Cloud Gateway 项目。
以上假定网关具有与这些类似的路由(仅限片段):
...
uri: "http://domain-1.com"
predicates:
- Path=/static
...
uri: "http://domain-2.com"
predicates:
- Path=/v1/api/foo
...
@apsisim 提供了使用网络代理的好主意。但他建议的工具不在任何 Maven 仓库中,并且具有商业许可。什么对我有用:
运行 网关,因此它将使用代理(你可以更花哨,找到一个空闲端口):
private const val proxyPort = 1080
@SpringBootTest(
properties = [
//"logging.level.reactor.netty.http.server=debug",
//"spring.cloud.gateway.httpserver.wiretap=true",
//"spring.cloud.gateway.httpclient.wiretap=true",
"spring.cloud.gateway.httpclient.proxy.host=localhost",
"spring.cloud.gateway.httpclient.proxy.port=$proxyPort"
]
)
然后使用模拟网络服务器作为代理
testImplementation("com.squareup.okhttp3:mockwebserver:4.2.1")
testImplementation("com.squareup.okhttp3:okhttp:4.2.1")
然后您的所有请求都将转到您的代理。请记住,http 协议指定对新服务器的第一个请求需要通过代理进行隧道传输,因此当您对网关进行第一个请求时,网关将向代理发送 2 个请求:
testClient.get()
.uri(path)
.header("host", gatewayDns)
.exchange()
nextRequestFromGateway {
method `should equal` "CONNECT"
headers[HOST] `should equal` "$realUrlBehindGateway:80"
}
nextRequestFromGateway {
path `should equal` "/api/v1/whatever"
headers[HOST] `should equal` realUrlBehindGateway
}
...
fun nextRequestFromGateway(block : RecordedRequest.() -> Unit) {
mockWebServer.takeRequest().apply (block)
}