一般spring 项目中的test 都是unit test, 只要用原生的spring test 的功能就足够了。但是如果想进一步, 做一个e2e flow 那就不是特别方便。 列如我要测试一个k8s的集群, 测试流量切换功能。 一般
create 一个原来的service
create 一个在k8s 的service
配置防火墙
创建监控器
进行流量切换。
一般一个流量切换的controller 开发者要进行测试的时候,需要把这5步都做完了。但是对流量切换的开发者前4步对他们来说超级麻烦。所以他们只能做个简单的controller单元测试,然后部署在在产线上,由实施的人去进行集成测试。 这样导致在产线上的人测出问题,再去找流量迁移的开发者去改bug。 来来回回效率超级低。那么就有个想法,如果让前4步自动化的情况下,会怎么样。 所以框架团队提供一个component, 让流量开发者只关注在第5步,前面的4 步自动完成。因此我们采取了如下方案。
@TestExecutionListener是TestContextFramework下的一个类级别注解,与@ContextConfiguration连用。
它的作用是:
在执行测试的声明周期中可以做一些自定义的事情。
并将当前测试方法的TestContext暴露出来。
@ContextConfiguration@TestExecutionListeners({CustomTestExecutionListener.class, AnotherTestExecutionListener.class}) class CustomTestExecutionListenerTests {    // class body...}翻译自 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。
在 spring.factories 里定义自己的 org.springframework.test.context.TestExecutionListener。
类的实现为
public class IntegrationTestExecutionListener extends AbstractTestExecutionListener {
 @Override
  public void beforeTestClass(TestContext testContext) throws Exception {
  }
}AbstractTestExecutionListener 有几个接口,在这里我只需要用到beforeTestClass
我们想实现这样一个能力,用户在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 {};
}实现一个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
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;