首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 开发语言 > 编程 >

运用测试度量代码质量

2013-03-12 
使用测试度量代码质量if (CARREFOUR.equals(supplier)) {return EUR} else if (CENTURY_MART.equals(supp

使用测试度量代码质量
if (CARREFOUR.equals(supplier)) { return EUR;} else if (CENTURY_MART.equals(supplier)) { return CNY;} else { throw new UnsupportedSupplierException(supplier);}

?类似这样的条件分支语句非常常见,其测试也并不难,将每种条件都覆盖到即可

?

?

@Testpublic void returnsEurWhenGetCurrenyGivenCarrefour() throws Exception {        assertEquals(EUR, target.getCurrencyBy(CARREFOUR));}@Testpublic void returnsCnyWhenGetCurrenyGivenCenturyMart() throws Exception {        assertEquals(CNY, target.getCurrencyBy(CENTURY_MART));}@Test(expected=UnsupportedSupplierException.class)public void throwsExceptionWhenGetCurrenyGivenUnknownSupplier() throws Exception {        target.getCurrencyBy(WE_DONT_KNOW);}

如果现在要支持一个新的供应商,那么可以增加一个测试用例,然后在条件分支中增加一个else if来实现,也就是说每新增一个供应商,我们就得新增一个测试,如果这种变化的频率比较高(噢,这是好事,说明生意做得不赖),代码和测试就会显得比较笨拙。让我们改进一下代码:

?

?

final String currency = currencies.get(supplier);if (currency != null) {    return currency;} else {    throw new UnsupportedSupplierException(supplier);  }

由于使用了Map作为实现,测试分支是固定的了,和供应商的增减无关了。

?

?

@Beforepublic void setupTestFixture() {        Map<string, String> currencies = new HashMap<String, String>();        currencies.put(MATCHED, CORRESPONSED);        currencies.put(ANOTHER, ANOTHER_CURRENCY);        target.setCurrencies(currencies);}@Testpublic void returnsCorresponsedCurrencyWhenSupplierNameMatched() throws Exception {            assertEquals(CORRESPONSED, target.getCurrencyBy(MATCHED));}@Test(expected=UnsupportedSupplierException.class)public void throwsExceptionWhenGetCurrenyGivenUnknownSupplier() throws Exception {        target.getCurrencyBy(WE_DONT_KNOW);}

?

?

二、静态方法对修改开放,对“测试”关闭

? ? ? ?使用静态方法非常简单,无需实例化并传递对象,在哪里都可以直接使用,但如果过度依赖它们,在测试中就不好办了。

?

public boolean preHandle(HttpServletRequest request, HttpServletResponse response) {     if (SessionUtils.isUnexpired(request)) {          return true;     } else {          //在response中输出alert语句     }}

? ? ? ?由于在静态方法中对保存在session中的属性做了计算和校验,要对其进行测试就需要事先在session中填充很多信息。

?

?

@Testpublic void returnTrueWhenUserSessionIsNotExpired() throws Exception {    MockHttpServletRequest request = new MockHttpServletRequest();    MockHttpSession session = new MockHttpSession();    request.setSession(session);    session.set......blablabla    assertTrue(target.preHandle(request));}@Testpublic void printAlertAndreturnFalseWhenUserSessionIsExpired() throws Exception {    MockHttpServletRequest request = new MockHttpServletRequest();    MockHttpSession session = new MockHttpSession();    request.setSession(session);    session.set......blablabla         assertFalse(target.preHandle(request, response));    assertEquals(ALERT, response.getContentAsString());}

? ? ? ?由于静态方法属于类,无法通过子类化并覆写的方式在测试时替换,即使测试重点并非SessionUtils.isUnexpired(req)本身,但由于它是一个必经步骤,你不得不为其准备完整的测试数据(哪怕非常麻烦)。让我们对它改造一下吧,不过我也并不想再实现一次isUnexpired(req),耍个小花招吧


运用测试度量代码质量
?再来看测试,就简单多了,我们可以用Stub/mock来替换SessionGateway的实现。

?

@Testpublic void returnTrueWhenUserSessionIsNotExpired() throws Exception {        context.checking(new Expectations() {        {               allowing(sessionGateway).isUnexpired(request);               will(returnValue(true));         }    });    assertTrue(target.preHandle(request, response));}@Testpublic void printAlertAndreturnFalseWhenUserSessionIsExpired() throws Exception {    context.checking(new Expectations() {        {               allowing(sessionGateway).isUnexpired(request);               will(returnValue(false));         }    });         assertFalse(target.preHandle(request, response));    assertEquals(ALERT, response.getContentAsString());}

?

小插曲、测试版的为什么组合优于继承

? ? 你可能已经见过各种版本的组合优于继承的理论和实例,下面我贡献一个测试版本的 :D


运用测试度量代码质量
? ? 先来看一下template版本的测试方案,我们需要分别对SubTemplate1和SubTemplate2的template()编写测试,并且由于AbstractTemplate的template()依赖SomeDependency和AnotherDependency,所以在以上两个测试中,我们需要使用Stub/Mock将它们替换掉,并为每一个测试准备一整套测试数据,即使它们的implementThis()方法只是return 1 和return 2。

? ? 再来看一下strategy版本的测试方案,我们需要为UsingStrategy编写测试,并用Stub/Mock替换Strategy、SomeDependency和AnotherDependency的实现,再分别为Strategy1、Strategy2编写测试。

? ? 你看,是不是strategy版本的测试方案的职责更清晰一些,每个测试都只负责被测试目标自己的代码,这种感觉在Template的子类特别多,Template包含的模板代码越是完整的时候越明显。

?

三、样板代码

? ? 有些代码吧,不管开发什么功能都要写的,比如经典的 service-dao,我们来看两个案例:

?

@Transactional  (1)@Overridepublic void cancel(String orderId) {    Order order = orderRepository.findBy(orderId);  (2)    order.cancel();    orderRepository.store(order);   (3)}@Transactional   (4)@Overridepublic void acknowledge(String orderId) {    Order order = orderRepository.findBy(orderId);  (5)    order.acknowledge();    orderRepository.store(order);   (6)}
这段代码实现了两个功能,取消订单和确认订单,我们假设其逻辑非常简单,Order对象自己就可以处理,其中(2)、(5) ?和 (3)、(6)是配对出现的,由于需要持久化数据,这两步总是免不了的。另外,比较隐蔽的是(1)、(4),这两行代码声明了事务,但很少有人去测试它们,而且经常会忘记添加它们。

?

?

@Testpublic void orderIsCanceledAfterCancelling() throws Exception {    final Order order = new OrderFixture().build();    final String orderId = order.getId();      context.checking(new Expectations() {        {             allowing(orderRepository).findBy(orderId);             will(returnValue(order));                          oneOf(orderRepository).store(order);        }   });   target.cancel(orderId);   assertTrue(order.isCanceled());}@Testpublic void orderIsAcknowledgedAfterAcknowledging() throws Exception {    final Order order = new OrderFixture().build();    final String orderId = order.getId();      context.checking(new Expectations() {        {             allowing(orderRepository).findBy(orderId);             will(returnValue(order));                          oneOf(orderRepository).store(order);        }   });   target.acknowledge(orderId);   assertTrue(order.isAcknowledged());}
?

?

其实,这两段代码并没有什么分支,要大费周张地测试有些多余。最好能够在满足覆盖率的情况下只测试order,尽可能减少所谓的“service”。利用Command模式,将“service”压缩到一个。


运用测试度量代码质量

@Testpublic void orderIsCanceledAfterCancelling() throws Exception {    final Order order = new OrderFixture().build();    final CancelOrderCommand command = new CancelOrderCommand();    command.handle(order);   assertTrue(order.isCanceled());}@Testpublic void orderIsAcknowledgedAfterAcknowledging() throws Exception {    final Order order = new OrderFixture().build();     final AcknowledgeOrderCommand command = new AcknowledgeOrderCommand();    command.handle(order);   assertTrue(order.isAcknowledged());}

?

经过修改后,在测试中我们不用再编写样板代码(同时产品代码中也不用了),由于Order、Command都可以通过构造函数实例化, 测试场景的准备就简单了。而且@Transactional忘记添加的问题也有所缓解(因为只需要一个“service”了)

?

?

四、对应代码抽象级别的测试

?

public void run() {    loggingSupport.info("start running, productId=" + productId);    if (a) {        ......    } else if (b) {        ......    } else {        ......    }    loggingSupport.info("end running, productId=" + productId);}
在它的三个测试中,都需要特意指定一下日志记录的内容:

??

                final String startText = "start running, productId" + productId;                final String endText = "end running, productId" + productId;context.checking(new Expectations() {{oneOf(loggingSupport).info(startText);                                                                // other expectations                                oneOf(loggingSupport).info(endText);}});                                target.run();
?

?

?出现这种情况的一个原因就是抽象层级没有把握好,在run()方法中,应该是关注的流程,而不是流程节点的实现。比如你在一个测试中编写了很多的断言(对模拟对象的预期定义也可以看作是一种断言),就很有可能说明了这种现象,最好通过引入更细粒度的对象来分担职责。这里我们不妨引入RunnerNotifer,让它来通知记录日志:

?

public void run() {    runnerNotifier.notifyStart(productId);    if (a) {        ......    } else if (b) {        ......    } else {        ......    }     runnerNotifier.notifyEnd(productId);}
这样修改之后,测试代码就不需要再拼接日志内容了:

?

?

context.checking(new Expectations() {{oneOf(runnerNotifier).notifyStart(productId);                                                                // other expectations                                oneOf(runnerNotifier).notifyEnd(productId);}});                                target.run();

?然后再单独为RunnerNotifier的实现添加测试即可。

?

? ? ? ? 本次就到这里,如果你有更好的想法,请务必跟贴让我知道,谢谢 :)

?

?

热点排行