正如【微服务实战:基于Spring Cloud Gateway + AWS Cognito 的BFF案例】一文中所介绍的,我司的微服务群采用了Spring Cloud Gateway作为API认证网关,利用Spring Security为API认证网关和后端微服务提供了OAuth认证功能。
当我们尝试测试与其他服务存在通信的微服务程序时,我们可以做以下两件事之一:
两者各有其优缺点。首先,对于端到端测试方式:
优点:
缺点:
其次,对于单体/集成测试方式:
优点:
缺点:
可以看出这两种测试其实是相辅相成,互相补充的。在项目工期和资源允许的情况下,两者都可以安排上。但在人员匮乏,工期紧张的时候,我个人倾向于后者。原因是我们可以快速实施测试,并快速得到反馈,缓解工期的紧张;另外还可以采取一些技术手段尽量避免它带来的缺点。比如:优先开发基于OAS(Open API Specification)的API规格,API Provider和Consumer都基于共同的OAS进行开发。这样可以减少测试桩和真正实现之间的差异。
本文的讨论将侧重于后者。
Spring Security文档中,建议采用WebTestClient
来测试基于Webflux的响应式程序。用它可以很方便的设置我们测试所需的各种认证信息。在如下测试类上使用了@AutoConfigureWebTestClient
进行自动配置。
另外,com.github.tomakehurst.wiremock.client.WireMock
可以帮助我们快速设置测试桩(Stub),在如下测试类上使用了@AutoConfigureWireMock(port = 0)
自动配置,此时port = 0
设置的端口是随机的,端口值可以通过${wiremock.server.port}
参数获取。
另外,我们指定了@ActiveProfiles("test")
,所以在执行这个测试类时application-test.yaml
中的配置会生效。
@AutoConfigureWebTestClient
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWireMock(port = 0)
@ActiveProfiles("test")
class ApiGatewayApplicationTests {
@Autowired
private WebTestClient client;
// ...
}
为了在测试时使用Stub的URI,我们需要改造一下Controller
,先将URI配置到application.yaml
中,然后通过@Value("${order-service-uri}")
获取。
@RestController
public class CompositionController {
@Value("${order-service-uri}")
private String orderServiceUri;
@Value("${storage-service-uri}")
private String storageServiceUri;
@GetMapping("/composition/{id}")
public Mono extends ResponseEntity>> proxy(@PathVariable Integer id, ProxyExchange> proxy) {
return proxy.uri(orderServiceUri + "/api/order/get/" + id)
.get(resp -> ResponseEntity.status(resp.getStatusCode())
.body(resp.getBody()))
.flatMap(re1 -> proxy.uri(storageServiceUri + "/api/storage/get/" + id)
.get(resp -> ResponseEntity.status(resp.getStatusCode())
.body(Map.of("order",re1.getBody(),"storage",resp.getBody()))));
}
}
同时,在如下application-test.yaml
中,将上述URI设置为Stub的地址,这里通过${wiremock.server.port}
参数获取随机端口。
account-service-uri: http://localhost:${wiremock.server.port}
order-service-uri: http://localhost:${wiremock.server.port}
storage-service-uri: http://localhost:${wiremock.server.port}
另外,在application-test.yaml
中还要做如下配置,首先我们不需要将Token传递给Stub,所以将spring.cloud.gateway.default-filters
的设置清空。另外,需要注册一个名为test
的客户端,这里不需要真实的配置信息。
spring:
cloud:
gateway:
default-filters: # set to empty
security:
oauth2:
client:
provider:
test: # add for testing
issuerUri: https://cognito-idp..amazonaws.com/_
user-name-attribute: username
registration:
test:
client-id: dummy-client-id
client-secret: dummy-client-secret
client-name: scg-cognito-sample-user-pool
provider: cognito
scope: openid
redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
authorization-grant-type: authorization_code
然后我们就可以在测试类中设置测试桩(Stub)了。下面的Stub会在我们访问http://localhost:${wiremock.server.port}/api/storage/get/123
时,返回我们预设的 Header 和 Body。
@BeforeAll
static void init() {
//Stubs
stubFor(get(urlEqualTo("/api/storage/get/123"))
.willReturn(aResponse()
.withBody("{"id":101,"commodityCode":"123","count":100}")
.withHeader("Content-Type", "application/json")));
// ...
}
启动测试应用程序上下文后,我们要向API网关发出经过身份验证的请求。 我们可以使用@WithMockUser(roles = "ADMIN")
之类的注解或者mutateWith
方法,它可以设置我们需要的任何属性。下面的例子中使用了mutateWith
方法构造了一个ID Token
,使得我们可以通过API网关的OAuth认证,如果不使用mutateWith
方法,API网关会返回401 Unauthorized
。
@Test
void testGetComposition() {
client.mutateWith(mockOidcLogin()
.idToken(builder -> builder.subject("Subject A")))
.get().uri("/composition/123").exchange()
.expectStatus().is2xxSuccessful();
}
我们还可以为测试用户添加任何权限(Authority),如下所示,我们增加了ROLE_account.access
权限,这样API网关可以通过hasRole("account.access")
的鉴权校验,否则API网关会返回403 Forbidden
。
@Test
void testGetHomeAuthenticated() {
client.mutateWith(mockOidcLogin()
.idToken(builder -> builder.subject("Subject A"))
.authorities(new SimpleGrantedAuthority("ROLE_account.access"))
)
.get().uri("/api/account/whoami").exchange()
.expectStatus().is2xxSuccessful()
.expectBody().jsonPath("$.['account.access']").isEqualTo("/api/account/**");
}
对于后端微服务的测试就相对简单一些了,我们只需要在微服务上验证JWT令牌
。 我们在Account服务中使用 Spring Security 5.2 中引入的新 jwt() RequestPostProcessor
来轻松更改 JWT 特性。
@SpringBootTest
@AutoConfigureMockMvc
class AccountControllerTest {
@Autowired
private MockMvc mockmvc;
private final String base_url = "/api/account";
@Test
void whoami() throws Exception {
mockmvc.perform(get(base_url + "/whoami").with(jwt().jwt(builder -> builder.subject("Subject A"))))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Subject A"));
}
}
本文着重介绍了微服务的单体/集成测试方法,我们使用WireMock
设置了测试桩,并使用mutateWith
和jwt()
设置了OAuth认证所需的各种信息。如果对你有所帮助,请点赞订阅分享,感谢!
微服务实战:基于Spring Cloud Gateway + AWS Cognito 的BFF案例
如果文章对你有帮助欢迎关注公众号