如何正确地吐槽

 

李祥乾

风控RD & QA

 

Test fails on case
java.lang.AssertionError
expected: 单元测试
ACTUAL: 吐槽

 

 

如何正确地单元测试

 

李祥乾

风控RD & QA

 

Unit Test的意义

从case说起

Text

还是这个case

public GpsLocationInfo getInfoByLocation(Object location) {
    GPSLocation gpsLocation = null;
    if (location instanceof Map) {
        gpsLocation = getGPSLocation((Map<String, String>) location);
    } else if (location instanceof String) {
        gpsLocation = getGPSLocation((String) location);
    }

    if (null == gpsLocation) {
        return null;
    }
    GpsLocationInfo gpsLocationInfo = new GpsLocationInfo();
    LocationCoordinate locationCoordinate = new LocationCoordinate
            (gpsLocation.getLatitude(),gpsLocation.getLongitude());
    try {
        gpsLocationInfo = getGpsLocationInfo(locationCoordinate);
    } catch (Exception e){
        //deal with exception
    }
    return gpsLocationInfo;
}

先不说java和Junit

Spock是一个JVM上基于动态语言Groovy的单元测试框架

Text

class GpsInfoServiceTest extends Specification {
    def "getInfoByLocation specifications"() {
        setup: "Setup the service"
        GpsInfoService service = new GpsInfoService()

        when: "Given an argument of a type that cannot be handled"
        def result = service.getInfoByLocation(new Integer(4))

        then: "It should return null"
        result == null

        when: "An empty string is given"
        result = service.getInfoByLocation("")

        then: "return null"
        result == null

        when: "A valid type argument is given but is invalid logically"
        def argStr = "333333 -34232544"
        def serviceMock = Mock(GpsInfoService)
        serviceMock.getGpsLocationInfo(_ as LocationCoordinate) >> { throw new                             
             Exception() } //This is a MOCK.
        result = serviceMock.getInfoByLocation(argStr)

        then: "An exception is handled internally and return null"
        result == null
    }
}

关于Spock多说几句

  • Specifications as Documentations
  • 可以看做是语法糖,基于JUnit和Groovy编写的DSL语言。
  • 可以混合写java和groovy,几乎无门槛。
  • 很方便地mock,spock里叫Interactions。
  • 有Spring插件。

单元测试,是什么?

  • In computer programmingunit testing is a software testing method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use.                                             -----From wikipedia
  •  
  • Unit test as specifications as documentations
    • 不仅仅是:自动化检查,防止可能的缺陷与错误。
    • 写Utest是一种设计行为编写文档的行为
      • 全部可以预见的假设环境与输入下的期待(expected)输出。
      • 作为可读的文档给调用者和其他开发人员阅读。

单元测试,为什么

  • Utest帮助程序变得可测试 -> 提高代码质量
    • 按照逻辑分解冗长的函数 -> 模块化代码
    • 需要mock外部依赖 -> 程序与周边的环境解耦
    • 需要开发者自己使用接口 -> 站在API使用者角度思考,更加关注函数的接口的可用性
  • 重构的前提是保证被重构的方法、类有完备的单元测试,保证重构前与重构后的行为一致。   ----《重构》
  • TTD(Test Driven Development),先写函数的测试方法,再实现功能。(先写文档,再写方法)。
  • TTD倡导的理念是:实现代码是刚好能够通过测试的最简单代码。
  •  

关于单测覆盖率

刻意追求高的单测覆盖率是不对的。

测试覆盖是一种“学习手段”。学习什么呢?学习为什么有些代码没有被覆盖到,以及为什么有些代码变了测试却没有失败。理解“为什么”背后的原因,程序员就可以做相应的改善和提高,相比凭空想象单元测试的有效性和代码的好坏,这会更加有效。

                                                  ----测试覆盖率有什么用?

单元测试的现状

单测行覆盖率

单测存在的问题

  • 可读性差单元测试里缺乏文字说明信息,只有assertTrue,assertEquals。
  • 考虑的环境因素、依赖与可能的输入情况不全面。如:http请求只mock了success,对于超时等其他情况没有对应的case。
  • 包含了外部依赖的方法缺乏单元测试Q3中tt删除的单测方法是存在外部依赖的方法。
  • 依赖Spring框架的代码缺乏单元测试。

如何单元测试

mock with mockito

Verify with mockito

@RunWith(MockitoJUnitRunner.class)  
public class ServiceTest{

    @Test
    public void methodTest() {
        //verify method was called
        Service service = Mockito.mock(Service.class);

        //do some tests....

        //verifying
        Mockito.verify(service).method(args..);

        //verify method was never called
        Mockito.verify(service, Mockito.times(0)).method(args..);

        //verify method was called 3 times
        Mockito.verify(service, Mockito.times(3)).method(args..);
    }
}

Mockito cannot

  • 无法mock final类
  • 无法mock static方法
  • 无法mock final方法
  • 无法mock private方法
  • 无法mock hashCode, equals(这些方法也不应该被mock)

 

Powermock can

  • mock static方法
  • mock final方法/类
  • mock 构造器
  • mock private方法
  • 兼容Mockito API,结合Mockito使用。

 

powerMOCK Private methods

@RunWith(PowerMockRunner.class)
@PrepareForTest(RiskService.class)
public class RiskServiceTest {
    @Test
    public void methodTest(){
        RiskService riskService = PowerMockito.spy(new RiskService());
        PowerMockito.when(riskService, "privateMethod", arg0, arg1, ...)
                .thenReturn(...);
    }
}

Powermock static methods

@RunWith(PowerMockRunner.class)
@PrepareForTest(RiskHelper.class)
public class RiskService {
    @Test
    public void methodTest(){
        RiskService riskService = new RiskService();
        PowerMockito.mockStatic(RiskHelper.class);
        PowerMockito.when(RiskHelper.staticMethod(arg...)).thenReturn(...);
        ...
    }
}

Mock with Spring

@Service
public class RiskService {
    @Autowired
    private FingerPrintService fingerPrintService;

    public String getImsiByFingerprintDecode(String fingerprint,String version, Integer app, 
            Integer platform) {
        String imsi = null;
        if (CollectionsUtil.empty(fingerprint) || CollectionsUtil.empty(version) || (app == null)
                || (platform == null)) {
            return imsi;
        }

        Map fingerprintDecode = fingerPrintService.decryptFingerprint(fingerprint, version, app, 
                platform);
        if (CollectionsUtil.empty(fingerprintDecode)) {
            return imsi;
        }
        if (fingerprintDecode.containsKey("imsi")) {
            imsi = (String) fingerprintDecode.get("imsi");
        }

        return imsi;
    }
}

Spring Way

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("unit-test-spring-config.xml")
public class RiskServiceTest {
    @Autowired
    RiskService riskService;

    @Autowired
    FingerPrintService fingerPrintServiceMocked;

    @Test
    public void getImsiByFingerprintDecodeTest() {
        String args = ...;
        when(fingerPrintService.decryptFingerprint(args)).thenReturn(...);
        //more test
    }
}
<beans...>
  <context:component-scan base-package="com.utest.example"/>

  <bean id="fingerPrintServiceMock" class="org.mockito.Mockito" factory-method="mock">
    <constructor-arg value="com.utest.example.FingerPrintService" />
  </bean>
  <bean id="riskService" class="com.utest.example.RiskService">
    <property name="fingerPrintService" ref="fingerPrintServiceMock" />
  </bean>
</beans>

Simper way

//object to test
RiskService riskService = new RiskService();

FingerPrintService fingerPrintService = MockUtil.mockField(riskService, "fingerPrintService");

when(fingerPrintService.decryptFingerprint(args)).thenReturn(...);
//or
when(fingerPrintService.decryptFingerprint(illArgs)).thenThrow(new Expcetion());



  • 会在risk-util里新添MockUtil类,方便大家mock或者单元测试。
  • MockUtil不仅仅用于Spring的mock,对对象的任意域都可以mock,包括private。
  • 有什么新的需求都可以提。

All in SPOCK WAY

import spock.lang.Specification

class GpsInfoServiceTest extends Specification {

    def "getInfoByLocation specifications"() {
        setup: "Setup the service"
        def serviceMock = Mock(GpsInfoService)

        when: "A valid type argument is given but is invalid logically"
        def argStr = "333333 -34232544"
        serviceMock.getProphetGpsLocationInfo(_ as LocationCoordinate) >> { throw new
                ProphetException() } //This is a MOCK.
        result = serviceMock.getInfoByLocation(argStr)

        then: "An exception is handled internally and return null"
        result == null
    }
}
  • Spock -> nonfinal classes mocking
  • 不支持static,final,private(final)。
  • Spock可以混合写groovy和java,使用PowerMock也很容易。

Another CASE

public void methodToTestWithBlockingDependencies(args...) {
    //...some params checking
    //A external dependency code block, not easy to mock externally.
    HttpConnection conn = ...
    
    //some code to deal with results
}

参数检查 -> Http请求 -> 阻塞 -> 处理结果

  • 对它进行单元测试,需要mock外部依赖。
  • 需要使用extract method重构手法,将外部依赖的代码抽出来到新的方法之中。
  • 然后mock这个方法,对原方法测试。
  • 单元测试帮助思考代码,重构代码。

什么时候需要mock

  • 外部依赖,of course.
  • 框架依赖。
  • 一段耗时较长的方法调用,需要mock掉。->测试的是方法本身的逻辑。
  • 调用普通方法时比较难以触发的结果,可以使用mock模拟那个结果。

单元测试的界限

  • 只测试本函数的逻辑,应当迅速完成,不应当有任何等待、阻塞操作(IO,数据库访问,http请求等,即外部依赖)。
  • 对外部依赖的可能的所有情况进行假设(mock),测试本函数能够做出如设计一致的反馈。
  • 否则,是集成测试的范畴。

对大家来说,这些都是小菜一碟。

  • 写代码时候多走一步,比出了问题排查缺陷、Bug时候一层一层定位、排除、分析、吐槽、祈祷pass容易多了。
  •  
  • 最重要的是,帮助自己思考用更好的方式实现,设计更合理、清晰的api,降低组件间耦合,降低了teamwork的沟通成本。

如何提升我们的单测质量,还有工程质量

Code REview

  • Reviewers提醒PRer对添加、修改的方法添加单元测试。
  • 建议Pull Request时候添加工程组成员review。

 

  • 对方法的外部依赖没有mock的情况必须修改。
  • 对常见的非法输入在单测中需要有对应的case覆盖
  • 对于调用了可能抛出exceptions的方法的方法,单测中对于exception需有case覆盖
  • 单测应当包含必要的文字说明,理想状态下调用者可以参考测试代码使用api。

上线前的单元测试

  • PR前pull + mvn clean test
  • 建议上线merge到master前pull + mvn clean test
  • 正在做一个命令行工具,简化与规范化上线流程(pull,test,PR,merge)。

long way to go

  • 目标永远是工程质量而不是单测覆盖率。
  • Q4计划包含搭建集成测试环境和压力测试环境,但首先要把单元测试做好。
  • First
    • 通过code reviews提高上线代码的质量。
    • 在risk-util里添加方便单测与mock的Util类。
    • 提高risk-util的单测覆盖率,试验Spock,分享一些cases给大家。
  • Then
    • 喜欢尝鲜的同学可以试用Spock测试框架。
    • PR和上线前必须跑过单测(讨论点)。

QA

means Questions & Answering.

UnitTest

By Xiangqian Lee

UnitTest

  • 781