工作多年后我更明白了UT的重要性(上)

简介: 对于有经验的开发写单元测试是非常有必要的,并且对自己的代码质量以及编码能力也是有提高的。单元测试可以帮助减少bug泄露,通过运行单元测试可以直接测试各个功能的正确性,bug可以提前发现并解决,由于可以跟断点,所以能够比较快的定位问题,比泄露到生产环境再定位要代价小很多。同时充足的UT是保证重构正确性的有效手段,有了足够的UT防护,才能放开手脚大胆重构已有代码,工 作多年后更了解了UT,了解了UT的重要性。 

单元测试  


在敏捷的开发理念中,覆盖全面的自动化测试是添加新特性和重构的必要前提。单元测试在软件开发过程中的重要性不言而喻,特别是在测试驱动开发的开发模式越来越流行的前提下,单元测试更成为了软件开发过程中不可或缺的部分。同时单元测试也是提高软件质量,花费成本比较低的重要方法。


1.单元测试的时机和测试点


1.1单元测试的时机


  1. 在业务代码前编写单元测试采用测试驱动开发,这是我们经常使用和推荐的。


  1. 在业务代码过程中进行单元测试,对重要的业务逻辑和复杂的业务逻辑进行添加测试。


  1. 在业务逻辑之后再编写测试是我们不建议的,除非对遗留代码的修改,需要先进行测试用例的添加,保证我们修改和重构后的代码不会破坏之前的业务逻辑。


1.2单元测试的测试点


  1. 在逻辑复杂的代码中添加测试。


  1. 在容易出错的地方添加测试。


  1. 不易理解的代码中添加测试,在以后看到测试就可以非常清楚代码要实现的逻辑。


  1. 在考虑后期需求变更相对较大的代码中添加测试,这样后期需求更变修改代码之后就不用太担心写的代码对不对以及是否破坏了已有代码逻辑。


  1. 外部接口处添加解耦代码、同时增加单元测试。


2.代码不可测试性的根源


  1. 代码中调用到了底层平台的接口或只有系统运行后才能获得的资源(数据库连接、发送邮件,网络通讯,远程服务, 文件系统等)但业务代码与这些资源未解耦。这样在测试代码需要创建这个类的时候会去初始化这些资源时导致无法测试。


  1. 在方法内部new一个与本次测试无关的对象。


  1. 代码依赖层次很深,逻辑复杂,一次方法的往往要调用N次底层的接口,或者类的方法非常多。这样的代码我们需要对类进行重构,尽量保证类的单一职责:这个类在系统中的意图应当是单一的,且修改它的原因应该只有一个。


  1. 使用单例类和静态方法,并且单例类和静态方法使用到了我们底层的接口或者其他接口。


3.测试工具使用和测试方法介绍


在做单元测试的时候,我们会发现我们要测试的方法会引用很多外部依赖的对象,如调用平台接口、连接数据库、网络通讯、远程服务、FTP、文件系统等等。 而我们没法控制这些外部依赖的对象,为了解决这个问题,我们就需要用到Mock工具来模拟这些外部依赖的对象,来完成单元测试。 现在比较流行的Mock工具有JMock、EasyMock、Mockito、PowerMock。我们使用的是Mockito和PowerMock。PowerMock弥补了其他3个Mock工具不能mock静态、final 、私有方法的缺点。 在下面的情况下我们可以使用Mock对象来完成单元测试。


  1. 实对象具有不可确定的行为,会产生不可预测的结果。 如:数据库查询可以查出一条记录、多条记录、或者返回数据库异常等结果。


  1. 真实对象很难被创建。如:平台代码,或者Web、JBoss容器等。


  1. 真实对象的某些行为很难触发。 如:代码中需要处理的网络异常、数据库异常、消息发送异常等。


  1. 真实情况令程序运行很慢。 在敏捷的实践中我们完成了CI,在开发提交代码前需要执行整个项目的单元测试用例,只有测试通过才可以提交代码。这就要求我们每个单元测试用例需要尽可能的短,整个项目的测试时间才会短。当有的测试用例需要测试大数据量情况下系统的预期时,就需要使用Mock对象。


  如我们代码中需要判断只有当系统的缓存队列大于40000时,我们开始考虑丢弃非关键的消息,当超过48000时,需要只处理最重要的消息,当超过50000时需要丢弃全部消息。此时就需要对此缓存队列进行Mock,根据调用返回不同的数据量给测试。 5. 测试需要知道真实对象是如何被调用的。如:测试用例需要验证是否发送了JMS,此时就可以通过Mock对象是否被调用来测试。 6. 真实对象实际不存在时。 如:当我们与其他模块交互时,或者与新的接口打交道时,更有就是对方的代码还没有开发完毕时,我们可以通过Mock来模拟接口的行为,实现代码逻辑的验证和测试。


3.1 Mocktio简单使用说明


mock可以模拟各种各样的对象,从而代替真正的对象做出希望的响应。


1、模拟对象的创建

List cache = mock(ArrayList.class);
System.out.println(cache.get(0));
//-> null 由于没有对mock对象给预期,所以返回都是null

2、模拟对象方法调用的返回值

List cache = mock(ArrayList.class);
when(cache.get(0)).thenReturn("hello");
System.out.println(cache.get(0));
//-> ello

3、模拟对象方法多次调用和多次返回值

List cache = mock(ArrayList.class);
when(cache.get(0)).thenReturn("0").thenReturn("1").thenReturn("2");
System.out.println(cache.get(0));
System.out.println(cache.get(0));
System.out.println(cache.get(0));
System.out.println(cache.get(0));
//-> 0,1,2,2 如果实际调用的次数超过了预期的次数,则会一直返回最后一次的预期值。

4、模拟对象方法调用抛出异常    

List cache = mock(ArrayList.class);
when(cache.get(0)).thenReturn(new Exception("Exception"));
System.out.println(cache.get(0));

5、模拟对象方法在没有返回值时也可以抛异常

List cache = mock(ArrayList.class);
doThow(new Exception("Exception")).when(cache).clear();

6、模拟方法调用时的参数匹配

AnyInt的使用,匹配任何int参数
List cache = mock(ArrayList.class);
when(cache.get(anyInt())).thenReturn("0");
System.out.println(cache.get(0));
System.out.println(cache.get(2));
//-> 0,0

7、模拟方法是否被调用和调用的次数,预期调用了一次

List cache = mock(ArrayList.class);
cache.add("steven");
verify(cache).add("steven");
复制代码

预期调用了两次入缓存,没有调用清除缓存的方法

List cache = mock(ArrayList.class);
cache.add("steven");
cache.add("steven");
verify(cache,times(2)).add("steven");
verify(cache,never()).clear();


还可以通过atLeast(int i)和atMost(int i)来替代times(int i)来验证被调用的次数最小值和最大值。【注意】Mock对象默认情况下,对于所有有返回值且没有预期过的方法,Mocktio会返回相应的默认值。对于内置类型会返回默认值,如int会返回0,布尔值返回false。对于其他type会返回null。mock对象会覆盖整个被mock的对象,因此没有预期的方法只能返回默认值。这个在初次使用Mock时需要注意,经常会发现测试结果不对,最后才发现自己未给相应的预期。


3.2 PowerMock简单使用说明


PowerMock使用一个自定义类加载器和字节码操作来模拟静态方法,构造函数,final类和方法,私有方法,去除静态初始化器等等。 PowerMock使用简单,在类名前添加注解,在预期前调用PowerMock的mock静态类方法,其他的预期方法和Mockito类似。

@PrepareForTest(System.class)
@RunWith(PowerMockRunner.class)
public class Test {
@org.junit.Test
public void should_get_filed() {
    System.out.println(System.getProperty("myName"));
    PowerMockito.mockStatic(System.class);
    PowerMockito.when(System.getProperty("myName")).thenReturn("steven");
    System.out.println(System.getProperty("myName"));
    //->null steven
    }
}


3.3 Fake对象的使用


测试中需要模拟对象,除了常用的mock对象外,我们还会经常用到Fake对象。Mock对象是预先计划好的对象,带有各种期待,他们组成了一个关于他们期待接受的调用的详细说明。而Fake对象是有实际可工作的实现,但是通常有一些缺点导致不适合用于产品,我们通常使用Fake对象在测试中来模拟真实的对象。 在测试中经常会发现我们需要使用系统或者平台给我们提供的接口,在测试中我们可以新创建一个类去实现此接口,然后在根据具体情况去实习此模拟类的相应方法。


如我们创建了自己的FakeLog对象来模拟真实的日志打印,这样我们可以在测试类中使用FakeLog来代替代码中真实使用的Log类,可以通过FakeLog的方法和预期的结果比较来进行测试正确性的判断。


Fake对象和mock对象还有一个实际中使用的区别,Fake对象我们构造好后,以后所有的代码都去调用此Fake对象就可以了,不用每个类每次都要给预期。从这个角度可以看到当一个类的方法或者预期相对不变时,可以采用Fake对象,当这个类的返回信息预期变化非常不可预期时,可以采用MOCK对象。


3.4Mock服务的两种方式


(1)直接注入:用于类之间的依赖层次较多的情况,测试整个业务流程,粒度大。

Service service = mock(Service.class);
new Processor().process(service );
复制代码

(2)重写protected方法返回mock对象:用于类直接依赖于该服务的情况,测试行为的细节,粒度小。

Service service = mock(Service .class);
generator = new Generator() {
    @Override
    protected Service getService() {
        return service;
    }
}


3.5测试异常


Throwable有两个直接子类:Exception和Error


1、expcetd=SomeExecption.class

@Test(expected = AssertionError.class)
public void should_occur_assertion_error_when_xx() throws Exception {
    new Processor().process();
}
@Test(expected = NumberFormatException.class)
public void should_throw_number_format_exception_when_xx() {
    Convert.convert2Long();
}
复制代码

2、try-catch-fail只能用于Exception,Error不能用此种方式

try {
    method.invoke();
    fail();
} catch (Exception e) {
    assertTrue(e.getCause() instanceof RuntimeException);
}


3.6私有方法—采用反射来调用


@Test
public void should_throw_runtime_exception_when_check_data_fail() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    Method method = Generator.class.getDeclaredMethod("check", Item.class);
    method.setAccessible(true);
    try {
        method.invoke(Generator, mock(Item.class));
    } catch (Exception e) {
        assertTrue(e.getCause() instanceof RuntimeException);
    }
}


4.单元测试的格式


4.1测试类结构


public class ExampleTest {
    @BeforeClass
    public static void setUp() throws Exception {
        initGlobalParameter();
        registerServices();
    }
    @Before
    public void setUp() throws Exception {
        initGlobalParameter();
        registerServices();
    }
    @After
    public void tearDown() throws Exception {
        ServiceAccess.serviceMap.clear();
        clearCache();     }
    @AfterClass
    public static void tearDown() throws Exception {
        ServiceAccess.serviceMap.clear();
        clearCache();
    }
    @Test
    public void should_get_some_result1_when_give_some_condition1{
    }
    @Test
    public void should_get_some_result2_when_give_some_condition2{
    }
}

JUnit4是JUnit框架有史以来的最大改进,其主要目标便是利用Java5的Annotation特性简化测试用例的编写。先简单解释一下什么是Annotation,这个单词一般是翻译成元数据。元数据是什么?元数据就是描述数据的数据。也就是说,这个东西在Java里面可以用来和public、static等关键字一样来修饰类名、方法名、变量名。修饰的作用描述这个数据是做什么用的,差不多和public描述这个数据是公有的一样。


  • @Before:每个测试方法执行之前都要执行一次。
  • @After:before对应,每个测试方法执行之后要执行一次。
  • @BeforeClass:在所有测试方法之前运行,只运行一次。一般在此类中申请昂贵的外部资源。父类中有@BeforeClass方法,在其子类运行之前也会运行。
  • @AfterClass:与BeforeClass对应,在所有测试结束后,释放BeforeClass中申请的资源。 注意:@Before,@After,@BeforeClass,@AfterClass 标示的方法一个类中只能各有一个
  • @Test: 告诉JUnit,该方法要作为一个测试用例来运行。


4.2测试代码的位置


在Java中一个包可以横跨两个不同的目录,所以我们的测试代码和产品代码放在同一目录中,这样维护起来更方便,测试代码和产品代码在同一个包中,这样也减少了不必要的包引起,同时在测试类中使用继承更加的方便。


4.3测试用例格式3段式


一个测试用例主体内容一般采用三段式:given-when-then

  • Given:构造测试条件;
  • When:执行待测试的方法;
  • Then:判断测试结果是否符合期望。

 例如:

@Test
public void should_get_correct_result_when_add_two_numbers() {
    int a = 1;
    int b = 2;
    int c = MyMath.add(a, b);
    assertEquals(3, c);
}


4.4类名的命名方式


测试类的名称以Test结尾。从目标类的类名衍生出其单元测试类的类名。类名前加上Test后缀。 Fake(伪类)放在测试包中,使用前缀Fake。


4.5方法名的定义方式


should …do something…when…under some conditions…

例如:

should_NOT_delete_A_when_exists_B_related_with_A
should_throw_exception_when_the_parameter_is_illegal


4.6业务代码中为测试提供的方法的注解


在业务代码中为了测试而单独提供的保护方法或者其他方法,我们通过@ForTest来标注。FofTest类如下:

@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE})
public @interface ForTest {
    String description() default "";
}



目录
相关文章
|
6月前
|
芯片
2023年的技术总结和工作反思
一、回顾2023年 回顾自己的2023年,还是发生了很多的变化。在大学毕业,就来到了芯翼参加工作,在这里也遇到了很多的前辈和小伙伴,收获工作的同时也收获了友情。但是,随着公司发展战略的变化,公司的人员架构也变额很多,对于我们刚毕业的大学生也变得越来越不友好,其实我也清楚这就是社会的发展现状。 其实,这不是我最终产生离职想法的结果,最终让我决定离职的是公司新来的人事主管十分的不理解我们,总是处处针对我们,这对于专心搞技术研发的我们来说,无疑是一个定时炸弹,让我们觉得自己的工作没有意义,甚至是没有成绩和结果,总是挂在嘴边的KPI考核也是越来越严格,总是觉得刚毕业的大学生的能力不行之类的,话说谁
|
5月前
|
数据建模
技术经验解读:ZVS振荡电路工作原理分析
技术经验解读:ZVS振荡电路工作原理分析
102 1
|
Arthas 消息中间件 人工智能
为什么很多人工作3年,却只有1年经验?
同样是在软件开发行业工作 3 年,为什么有些人经验丰富,可以独当一面,而有些人却还和工作一年的人差不多?作者给出了自己的答案。
48337 16
|
5月前
|
算法 Java 大数据
为什么很多人工作 3 年 却只有 1 年 经验?
为什么很多人工作 3 年 却只有 1 年 经验?
67 0
|
11月前
|
设计模式 运维 分布式计算
工作经验小结(2023.11.21)
工作经验小结(2023.11.21)
180 1
|
程序员 开发工具
[老文拾遗]如果我当上技术经理如何展开工作(三)
[老文拾遗]如果我当上技术经理如何展开工作(三)
|
XML Java 测试技术
工作多年后我更明白了UT的重要性(下)
对于有经验的开发写单元测试是非常有必要的,并且对自己的代码质量以及编码能力也是有提高的。单元测试可以帮助减少bug泄露,通过运行单元测试可以直接测试各个功能的正确性,bug可以提前发现并解决,由于可以跟断点,所以能够比较快的定位问题,比泄露到生产环境再定位要代价小很多。同时充足的UT是保证重构正确性的有效手段,有了足够的UT防护,才能放开手脚大胆重构已有代码,工 作多年后更了解了UT,了解了UT的重要性。 
377 0
|
测试技术 芯片 异构计算
研发感悟:从CPU架构图谈谈开发工作
研发感悟:从CPU架构图谈谈开发工作
173 0
研发感悟:从CPU架构图谈谈开发工作
|
机器学习/深度学习 数据采集 人工智能
2020年,这个算法团队都干了啥?
什么是算法?什么是广告算法工程师?算法工程师又是如何定义的?今天作者将就算法、电商算法为主题和我们分享他的理解,同时还将和我们分享ICBU算法团队的整体工作和2020年的一些重要技术突破。
2020年,这个算法团队都干了啥?

相关实验场景

更多
下一篇
无影云桌面