通常spring 项目中的test 都是unit test, 只要用原生的spring test 的功能就足够了。但是如果想进一步, 做一个e2e flow 那就不是特别方便。 比如我要测试一个k8s的集群, 测试流量切换功能。 通常
- create 一个原来的service
- create 一个在k8s 的service
- 配置防火墙
- 创建监控器
- 进行流量切换。
通常一个流量切换的controller 开发者要进行测试的时候,需要把这5步都做完了。但是对流量切换的开发者前4步对他们来说非常麻烦。所以他们只能做个简单的controller单元测试,然后部署在在产线上,由实施的人去进行集成测试。 这样导致在产线上的人测出问题,再去找流量迁移的开发者去改bug。 来来回回效率非常低。那么就有个想法,如果让前4步自动化的情况下,会怎么样。 所以框架团队提供一个component, 让流量开发者只关注在第5步,前面的4 步自动完成。因此我们采取了如下方案。
Test Lister
TestExecutionListener的定义与作用
@TestExecutionListener是TestContextFramework下的一个类级别注解,与@ContextConfiguration连用。
它的作用是:
- 在执行测试的声明周期中可以做一些自定义的事情。
- 并将当前测试方法的TestContext暴露出来。
@ContextConfiguration@TestExecutionListeners({CustomTestExecutionListener.class, AnotherTestExecutionListener.class}) class CustomTestExecutionListenerTests { // class body...}
Spring默认提供的TestExecutionListener实现类
翻译自 3.5.3. TestExecutionListener Configuration
- ServletMockExecutionListener 为mock WebApplicationContext提供Servlet相关的API。
- DirtiesContextBeforeModesTestExecutionListener 在before模式中,处理@DirtiesContext注解。
- ApplicationEventTestExecutionListener 用于处理ApplicationEvent。
- DependencyInjectionTestExecutionListener 用于处理依赖注入。
- DirtiesContextTestExecutionListener 用于处理after模式中的@DirtiesContext注解。
- **TransactionalTestExecutionListener **用于处理事务注解。
- SqlScriptsTestExecutionListener 用于处理@Sql注解。
- EventPublishingTestExecutionListener 向mock出来的ApplicationContext中增加test execution events。
自定义TestExecutionListener
在 spring.factories 里定义自己的 org.springframework.test.context.TestExecutionListener。
类的实现为
public class IntegrationTestExecutionListener extends AbstractTestExecutionListener {
@Override
public void beforeTestClass(TestContext testContext) throws Exception {
}
}
AbstractTestExecutionListener 有几个接口,在这里我只需要用到beforeTestClass
自定义TestExecutionListener的实现
定义Annotation
我们想实现这样一个能力,用户在testcase 上个annotation 就可以运行我们的lisener。
@RunWith(SpringRunner.class)
@SpringBootTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@EnvPrepare(properties = {"application=ABC", "sourcePool=sourcepoolid",
"manifest=buildpakcageidr"})
则定义annoation为
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface EnvPrepare {
/*
Properties in form {@literal key=value} that should be added
*/
@AliasFor("properties")
String[] value() default {};
@AliasFor("value")
String[] properties() default {};
}
定义相关的Bean
实现一个context bean, 这个context 可以传给后续的用户得到前几步做的信息。
public class IntegrationContext {
private CloudWorkload workload;
public IntegrationContext(){
workload = new CloudWorkload();
}
public CloudWorkload getCloudWorkload() {
return workload;
}
定义相关的IntegrationContextbean在一个Auto Configuration 里 名字为IntegrationInitializer。
@Configuration
public class IntegrationInitializer {
@Bean(name="integrationContext")
public IntegrationContext integrationContext(){
return new IntegrationContext();
}
}
在Auto Configuration定义好了之后,通过注册spring.factories 生效。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.integration.initializer.IntegrationInitializer
实现接口beforeTestClass
public void beforeTestClass(TestContext testContext) throws Exception {
}
在beforeClass 里主要做如下几步
- 取annotation 的值
estContext.getTestClass().getAnnotation(EnvPrepare.class);
- 获取相关操作的Bean 以及context ,
initBean
3.. 根据annotation的值进行操作 - 把操作结果放入context
public void beforeTestClass(TestContext testContext) throws Exception {
EnvPrepare envPrepare = testContext.getTestClass().getAnnotation(EnvPrepare.class);
if (envPrepare != null) {
Properties props = resolveParams(envPrepare.properties());
initBean(testContext);
IntegrationParams params = new IntegrationParams();
params.setManifest(String.valueOf(props.get(Constants.MANIFEST)));
params.setPaasRealm(String.valueOf(props.get(Constants.PAASREALM)));
params.setPoolId(String.valueOf(props.get(Constants.SOURCE_POOL_ID)));
params.setAppName(String.valueOf(props.get(Constants.APPLICATION)));
WorkloadComponent workloadComponent = new WorkloadComponent(altusProvService, mxapiService, cmsService);
AltusPool workload = workloadComponent.createWorkload(params);
context.getCloudWorkload().setNameSpace(workloadComponent.getNamespace(workload.getId(), workload.getPaasRealm()));
context.getCloudWorkload().setApplicationInstance(workloadComponent.getApplicationInstance(workload.getId()));
context.getCloudWorkload().setAltusPool(workload);
}
}
private void initBean(TestContext testContext) {
tessClient = (TessService) testContext.getApplicationContext().getBean("getTessService");
altusProvService = (AltusProvService) testContext.getApplicationContext().getBean("getProvService");
mxapiService = (MXAPIService) testContext.getApplicationContext().getBean("getCloudAPIService");
cmsService = (CMSService) testContext.getApplicationContext().getBean("getService");
context = (IntegrationContext) testContext.getApplicationContext().getBean("integrationContext");
}
为啥create service 之类的bean 没有定义,例如altusProvService = (AltusProvService) testContext.getApplicationContext().getBean("getProvService");
是怎么来的。这个是因为在被测试的项目中通常都有了。我们不需要再重复定义。 如果没有,当然要定义。
用法
对于test case 的人来说,他只要关注第5步就可以了。 他的test case 如下
@RunWith(SpringRunner.class)
@SpringBootTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@EnvPrepare(properties = {"application=ABC", "sourcePool=sourcepoolid",
"manifest=buildpakcageidr"})
public class AIMCreationIT {
private List<String> healthMonitors;
private String aimName = "cloud-e2e-aim";
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss z");
private static final ObjectMapper MAPPER = new ObjectMapper();
@Inject
private IntegrationContext integrationContext;
public void test2AIMCreate() throws IOException {
...
}
这样只要简单的配置个@EnvPrepare
就可以专心的写自己的test case了,所有的信息都可以通过IntegrationContext 来获得
@Inject
private IntegrationContext integrationContext;
网友评论