首页 > Java开发 > 外部服务强依赖的单元测试

外部服务强依赖的单元测试

  1. 背景:
    • 项目中使用Spring,在测试中需要初始化Spring容器管理的资源。
    • 开放平台开放接口,依赖其他业务系统的提供的接口,而JUnit的默认实现是单线程执行所有的测试用例。
  2. JUnit为什么是单线程执行测试用例?
    • 执行测试用例:
    •     /**
           * Do not use. Testing purposes only.
           */
          public Result run(Runner runner) {
              Result result = new Result();
              RunListener listener = result.createListener();
              notifier.addFirstListener(listener);
              try {
                  notifier.fireTestRunStarted(runner.getDescription());
                  runner.run(notifier);
                  notifier.fireTestRunFinished(result);
              } finally {
                  removeListener(listener);
              }
              return result;
          }

      很明显,关键方法为Runner的run方法,所以最关键就是Runner的实现。

    • JUnit4中默认的Runner的实现为BlockJunit4ClassRunner,如果单元测试中有@RunWith注解,则使用注解中Runner。
    • 关键实现之RunnerScheduler:默认是循环执行每一个测试用例,所以,Junit的默认实现是单线程的。
    •     private void runChildren(final RunNotifier notifier) {
              final RunnerScheduler currentScheduler = scheduler;
              try {
                  for (final T each : getFilteredChildren()) {
                      currentScheduler.schedule(new Runnable() {
                          public void run() {
                              ParentRunner.this.runChild(each, notifier);
                          }
                      });
                  }
              } finally {
                  currentScheduler.finished();
              }
          }
          private volatile RunnerScheduler scheduler = new RunnerScheduler() {
              public void schedule(Runnable childStatement) {
                  childStatement.run();
              }
      
              public void finished() {
                  // do nothing
              }
          };
  3. 多线程执行测试用例解决方案:
    • 上一节知道,关键的调度器RunnerScheduler的schedule方法,我们只要实现一个多线程调度器就可以实现:
    • @Slf4j
      public class ConcurrentRunner extends BlockJUnit4ClassRunner {
      
          public ConcurrentRunner(Class<?> klass) throws InitializationError {
              super(klass);
              ExecutorService executorService = Executors.newFixedThreadPool(5);
              List<Future> list = Lists.newArrayList();
              RunnerScheduler scheduler = new RunnerScheduler() {
                  public void schedule(Runnable childStatement) {
                      list.add(executorService.submit(childStatement));
                  }
      
                  public void finished() {
                      // do nothing
                      executorService.shutdown();
                      try {
                          executorService.awaitTermination(5, TimeUnit.MINUTES);
                      } catch (Exception e){
                          log.error("",e);
                      }
                  }
              };
              setScheduler(scheduler);
      
          }
      }
    • 简单的测试:
    • @RunWith(ConcurrentRunner.class)
      public class TestBB {
      
          @Test
          public void test1(){
              try {
                  TimeUnit.SECONDS.sleep(5);
              } catch (Exception e){
      
              }
              System.out.println(Thread.currentThread()+"===========");
      
              System.out.println("test1");
          }
          @Test
          public void test2(){
              try {
                  TimeUnit.SECONDS.sleep(5);
                  System.out.println(Thread.currentThread()+"===========");
      
              } catch (Exception e){
      
              }
              System.out.println("test2");
          }
      }
    • 这种方式,我们可以利用多线程执行我们减少执行单元测试的时间,但依然不能解决对外部服务强依赖的问题,外部服务不再线直接导致单元测试的失败。
  4. JMokit解决外部服务强依赖的问题:
    • JMokit可以Mock掉外部服务、数据库操作、异步消息通知等,让我们专注于单元测试,不用理会外部服务的依赖。在Spring中,Mock掉对资源的强依赖,我们可以不用在容器内测试,减少Spring容器初始化时间。
    • JMockit (https://code.google.com/p/jmockit/)完全基于 Java5 SE 的 java.lang.instrument 包开发,内部使用 ASM
      库来修改Java的Bytecode。正是由于基于instrument,可以修改字节码。

    • 使用:
      • 引入Maven依赖:
      • <dependency>
              <groupId>org.jmockit</groupId>
              <artifactId>jmockit</artifactId>
              <version>1.31</version>
              <scope>test</scope>
        </dependency>
        @RunWith(JMockit.class)
        public class OrderExportServiceTest {
        
            private OpenOrderExportService openOrderExportService =new OpenOrderExportService();
        
            @Test
            public void testExport(@Mocked ZookeeperPropertiesService zookeeperPropertiesService,@Mocked OpenRestClient client) {
                openOrderExportService.setZookeeperPropertiesService(zookeeperPropertiesService);
                OpenOrderExportMindTo to = new OpenOrderExportMindTo();
                to.setCommercialIds(Lists.newArrayList(247900002l));
                to.setEndDate("2017-03-21 23:32:20");
                to.setOrderDateType(1);
                to.setBrandId(2479l);
                to.setStartDate("2015-08-12 00:00:20");
                to.setStartId(1l);
                to.setPageSize(200l);
                //只查询已经定义的
                to.setSourceChild(StringUtils.collectionToCommaDelimitedString(Lists.newArrayList(OpenOrderSourceType.values()).stream().map(eto->eto.getKey()).collect(Collectors.toList())));
                to.setTradeStatus(StringUtils.collectionToCommaDelimitedString(Lists.newArrayList(4)));//只查询已经完成的订单
                new Expectations(){{
                    zookeeperPropertiesService.getPropertyValue("api.getTradeList");result = "http://";
                    String res = "{\"data\":{\"currentPage\":1,\"pageSize\":2,\"totalRows\":128,\"startRow\":0,\"totalPage\":64,\"items\":[{\"id\":38425903,\"bizDate\":\"2017-01-03\",\"tradeNo\":\"101161226023010877435556\",\"tableInfo\":\"456\",\"areaName\":\"大厅区\",\"deliveryTypeName\":\"内用\",\"deliveryType\":1,\"waiterName\":\"pengyn\",\"casherName\":\"pengyn\",\"saleAmount\":61.0,\"sourceChild\":1,\"sourceChildName\":\"ANDROID收银终端\",\"source\":10,\"sourceName\":\"商户收银终端\",\"tradePayStatus\":3,\"tradePayStatusName\":\"已支付\",\"deliveryStatus\":0,\"deliveryStatusName\":\"\",\"tradeStatus\":4,\"tradeStatusName\":\"已完成\",\"privilegeAmount\":-50.0,\"custShouldPay\":11.0,\"custRealPay\":11.0,\"decreaseOverflow\":0.0,\"refundAmount\":0.0,\"depositAmount\":0,\"refundDeposit\":0,\"receivableAmount\":11.0,\"tradeAmountBefore\":11.0,\"receivedAmount\":11.0,\"additionalAmount\":0.0,\"shopName\":\"南粉北面-敢动剁手-2479gx\",\"cashDeviceNo\":\"988\",\"tradeType\":1,\"tradeTypeName\":\"售货单\",\"serverCreateTime\":\"2016-12-26 15:30:11\",\"paymentTime\":\"2017-01-03 09:40:49\",\"deliveryFee\":0,\"deliveryPlatform\":1,\"deliveryPlatformName\":\"\",\"payName\":\"现金\",\"payModeId\":-3},{\"id\":38458641,\"bizDate\":\"2017-01-02\",\"tradeNo\":\"101170102092748531001003\",\"tableInfo\":\"桌台002\",\"areaName\":\"大厅区\",\"deliveryTypeName\":\"内用\",\"deliveryType\":1,\"waiterName\":\"pengyn\",\"casherName\":\"pengyn\",\"saleAmount\":63.0,\"sourceChild\":1,\"sourceChildName\":\"ANDROID收银终端\",\"source\":10,\"sourceName\":\"商户收银终端\",\"tradePayStatus\":3,\"tradePayStatusName\":\"已支付\",\"deliveryStatus\":0,\"deliveryStatusName\":\"\",\"tradeStatus\":4,\"tradeStatusName\":\"已完成\",\"privilegeAmount\":-11.0,\"custShouldPay\":52.0,\"custRealPay\":52.0,\"decreaseOverflow\":0.0,\"refundAmount\":0.0,\"depositAmount\":0,\"refundDeposit\":0,\"receivableAmount\":52.0,\"tradeAmountBefore\":52.0,\"receivedAmount\":52.0,\"additionalAmount\":0.0,\"shopName\":\"南粉北面-敢动剁手-2479gx\",\"cashDeviceNo\":\"988\",\"tradeType\":1,\"tradeTypeName\":\"售货单\",\"serverCreateTime\":\"2017-01-02 09:27:48\",\"paymentTime\":\"2017-01-02 09:39:14\",\"deliveryFee\":0,\"deliveryPlatform\":1,\"deliveryPlatformName\":\"\",\"payName\":\"现金\",\"payModeId\":-3}],\"brandId\":2479,\"startDate\":\"2015-01-01 00:00:00\",\"endDate\":\"2017-03-21 23:32:20\",\"orderDateType\":2,\"tradeStatus\":\"4,11,12\",\"sourceChild\":\"1,2,3,31,32,33,41,51,61,71,81,91,92,111,131,141,142,151,161,171,181,191\",\"sort\":1,\"commercialIds\":[247900002]},\"success\":true,\"message\":\"查询成功\"}";
                    OpenRestClient.postJson("http://", to);result = res;
                }};
        
                Pager<OpenOrderExportVo> dishBrandTypeDatas = openOrderExportService.getOrderExport(to);
            }
        }

        注意:采用@Mocked注解的对象,为JMockit生成的代理对象,和new Expections{{}}配合使用,即当执行其中的方法时,会返回仅跟其后的result。所以上面的示例中,zookeeperPropertiesService并不会从ZK上面获取任何属性,也不会调用外部服务。openOrderExportService.getOrderExport(to)中有两个方法会执行代理方法,方法中的其他路径都会测试到。

    • 在传统的Junit的单元测试中,@Test的方法中是不带有任何参数的,该参数是非法的,而在@RunWith(JMockit.class)的单元测试,为什么是合法的单元测试?
    • public final class MockFrameworkMethod extends MockUp<FrameworkMethod>

      会将FrameworkMethod的部分方法Mock掉,执行代理方法,即MockFrameworkMethod的带有@Mock的方法会执行代理方法。

    •  @Mock
         public static void validatePublicVoidNoArg(@Nonnull Invocation invocation, boolean isStatic, List<Throwable> errors)

      所以,验证参数时,执行了代理方法。

    • @Mocked:被修饰的对象将会被Mock,对应的类和实例都会受影响(同一个测试用例中)
    • @Injectable:仅Mock被修饰的对象
    • @Capturing:可以mock接口以及其所有的实现类
    • @Mock:MockUp模式中,指定被Fake的方法
  5. 更多JMockit信息详见:http://jmockit.org/

本文固定链接: http://www.devba.com/index.php/archives/6556.html | 开发吧

报歉!评论已关闭.