闭关修炼(二十)如何做好单元测试

本文详细介绍了单元测试的概念、组织测试用例的方式、JUnit常用断言、异常测试、参数化测试、超时测试以及Before和After等注解的使用。此外,还探讨了单元测试的优点和原则,并讲解了如何处理遗留代码。特别提到了Mock对象的重要性,包括MockServer和Mockito框架在模拟服务器和测试中的应用。
摘要由CSDN通过智能技术生成

很久很久以前拜读了一下狗头师兄的单元测试文章https://blog.csdn.net/qq_36652619/article/details/105364869(现在竟然收费了没得看了),于是写一下自己的心得吧



什么是单元测试

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。

使用单元测试框架JUni可以说让程序员按照规定去写测试用例,框架自动化地去执行测试代码。

按照规定是自己的约定,如测试类的类名其实叫什么无所谓,但是我们为了把测试内容和类关联起来,一般把要测试类的类名后加一个Test作为测试类的类名。

测试类中写测试方法,用Test注解修饰方法,用Assert.assertEquals方法帮助我们判断测试值和期待值是否一样。

举个例子,计算测试类

import org.junit.Assert;
import org.junit.Test;

public class CalculatorTest {

    @Test
    public void testCalculateResult1(){
        int result = 10 - 5;
        Assert.assertEquals(5, result);
    }
}

通过了就显示绿色
在这里插入图片描述

组织测试用例

那么我们有很多的测试用例该怎么组织呢?

Junit为了提供了Suite套件,帮助我们将测试用例一层一层的组织起来。

我们只要运行AllTest就能测试所有的测试用例

@RunWith(Suite.class)
@Suite.SuiteClasses({
        V1AllTest.class,
        V2AllTest.class,
        V3AllTest.class
})

public class AllTest {
}

在V1AllTest下面又可以嵌套Suite套件

@RunWith(Suite.class)
@Suite.SuiteClasses({
        V1v1Test.class,
        V1v2Test.class,
        V1v3Test.class

})
public class V1AllTest {

}

这样我们就能分层的进行测试,运行AllTest就把所有包的测试用例都进行测试,而我只想测试v1包的所有测试用例,我只要运行V1AllTest即可。结构分布如下图所示。这就可以进行不同粒度的测试。
在这里插入图片描述
这种Suite的组织方式就是设计模式中的组合模式。

Junit常用的断言

assertEquals(Object expected, Object actual)
断言两个对象相等。如果不是,则会在给定的消息中抛出assertion error。如果expected和 actual 为 null,将它们视为相等。(读了源码后发现,有趣的是如果传入的都是String类型会比较失败)

static public void assertEquals(String message, Object expected,
            Object actual) {
        if (equalsRegardingNull(expected, actual)) {
            return;
        } else if (expected instanceof String && actual instanceof String) {
            String cleanMessage = message == null ? "" : message;
            throw new ComparisonFailure(cleanMessage, (String) expected,
                    (String) actual);
        } else {
            failNotEquals(message, expected, actual);
        }
    }

assertTrue(boolean condition),断言条件为真。

    static public void assertTrue(boolean condition) {
        assertTrue(null, condition);
    }

assertNotNull(Object object)断言一个对象不为空。

static public void assertNotNull(String message, Object object) {
        assertTrue(message, object != null);
    }

ssertArrayEquals(Object[] expecteds, Object[] actuals)断言两个对象数组相等。

public static void assertArrayEquals(String message, Object[] expecteds,
            Object[] actuals) throws ArrayComparisonFailure {
        internalArrayEquals(message, expecteds, actuals);
    }

如何对Exception进行测试

在Test注解传入expected参数,如果try catch到的Exception是expected的类型,那么结果通过

 @Test(expected = ArithmeticException.class)
    public void testDenominatorIsZero() {
        int result = 10/0;
    }

结果显示:
在这里插入图片描述

如何进行参数化测试

如果待测的输入和输出是一组数据,我们就可以把测试数据组织起来,用不同的测试数据调用相同的测试方法。

例子:

/**
 * Math.abs测试
 *
 * @author uuz
 * @date 2021/01/22
 */
@RunWith(Parameterized.class)
public class AbsTest {
    int input;
    int expected;

    /**
     * 测试用例数据
     *
     * @return {@link Collection<?>}
     */
    @Parameterized.Parameters
    public static Collection<?> data() {
        return Arrays.asList(new Object[][]{
                {0, 0}, {1, 1}, {-1, 1}
        });
    }

    /**
     * 构造函数
     *
     * @param input    输入
     * @param expected 预期
     */
    public AbsTest(int input, int expected){
        this.input = input;
        this.expected = expected;
    }
    @Test
    public void testAbs(){
        int result = Math.abs(this.input);
        Assert.assertEquals(this.expected, result);
    }
}

输出结果:
在这里插入图片描述

在这个例子中,我们只写了一个测试方法,但是我们提供了三组参数,Junit利用我们提供的测试数据(用Parameters注解的data()方法)使用了三组不同的参数分别调用了同一个测试方法进行测试。

参数化测试要求:

  • 参数必须由静态方法data()返回,也就是说这个方法名是不可变的
  • 返回类型为Collection<Object[]>,也可以直接写一个<?>
  • 静态方法必须标记为@Parameters
  • 测试类必须标记为@RunWith(Parameteried.class)
  • 构造方法参数必须和测试参数对应

如何进行超时测试

我们可以为Junit的单个测试设置超时:
超时设置为1s:@Test(timeout=1000)

timeout单位是毫秒

注意:

  • 超时测试不能取代性能测试和压力测试

特殊注解

Before和After

这两个注解类似AOP吧

	@Before
    public void setUp(){
        // 每个测试用例执行前都会执行
    }
    
    @After
    public void tearDown(){
        // 每个测试用例结束后都会执行
    }

这样做的好处是什么?

让测试用例之间不相互依赖,如果某个测试用例对某个实例变量进行了修改,也就是说修改了共享变量的状态,这个实例变量恰巧被其他测试用例用到,就可能导致测试失败,所以用befor和after做初始化和收尾工作

BeforeClass和AfterClass

这两个注解,对这个测试类来讲,只会在开始前/后执行一次。总之只执行一次

用的很少,比如有个对象你只想初始化一次,并在整个类中使用

单元测试的优点

可以总结为三个方面

  • 验证行为:保证代码正确性,用于回归测试(在项目中让我们大胆的去修改程序结构,添加新功能),给重构带来保证
  • 设计行为:测试驱动迫使我们从调用者的角度去观察问题,迫使我们将代码设计为可测试的
  • 文档行为:单元测试某种程度是一种文档,精确的描述了代码行为,是教会我们如何使用函数和类的最佳文档

原则

单元测试是个团队行为,互相扶持共同前进的过程,也就是你运行别人测试代码,别人运行你的测试代码,相互验证正确性。

测试代码和代码同等重要,需要被同时维护,不但要重构代码, 也要重构测试单元测试。

保证单元测试的简单性和可读性。

单元测试是隔离的,测试用例间不能相互依赖,不能与时序耦合,也就是说能够以任何次序执行。

单元测试是可重复执行的,不能依赖于环境的变换,举个例子,你写了一个测试用例,文件读取在C盘,在你的机器能运行,而在别人的电脑这个文件不放在C盘,而在D盘,这样就造成运行失败,这种做法是不可取的。还有就是依赖时间的测试,比如说这个测试在这个时间段里可以通过测试,而过了一段时间又不能通过测试了,这样也是不可取的。

尽量对接口进行测试

应该可以迅速执行,不应该等待过长的时间,使用mock对象,对数据库和网络进行解耦。

应该可以自动化执行,集成到build过程中去。

使用mock对象

什么是mock?

Mock 测试就是在测试过程中,对于某些不容易构造或者不容易获取的比较复杂的对象,用一个虚拟的对象(Mock 对象)来创建以便测试的测试方法。

为什么要使用mock?

  • 真实的对象是不易构造的,例如httpservlet必须在servlet容器中才能被创建,比如开发人员写一个对接口HttpServletRequest的实现类,然后实现getParameter方法,不得不实现几十个无用的空方法。

  • 真是对象十分的复杂,比如jdbc的Connection和ResultSet

  • 真实对象的行为具由不确定性,异常点和边界点难以触发,很难模拟和控制他们的输出和返回结果。

  • 真实的对象可能还不存在,依赖的另一个模块还未开发完毕,如前后端分离,前端mock一个对象模拟API的调用。

总之,使用mock对象替代和冒充真实模块和被测试对象进行交换,帮助开发者可以精确的定制期待的行为。对TDD(Test-Driven Development)的有利支持

处理遗留代码策略

遗留代码是指与已经取消支持或维护的项目的源代码。很多人写项目不是从第一行开始写的,也就是说是在项目已有代码上继续写代码的。

那么如何对遗留代码重构并进行单元测试保证修改后系统的正确性(回归测试)呢?可以从以下方面入手:

  • 重构代码
  • 使用Mock Object 解除依赖
  • 测试分解,先写粗粒度测试,再写细粒度测试,Package->Class->method

处理遗留代码步骤:

  1. 确定要测试的类和函数
  2. 解除依赖(※ 可以用重构代码的方法)
  3. 编写测试用例
  4. 重构代码,灵活使用idea的extra method进行重构,创建子类重写抽取出的方法,实例对象改为自己的mock实现对象

使用code coverage工具检测覆盖率

模拟服务器MockServer

先下载moco.jar

我们只要执行Moco jar包,指定配置文件,就可开启一个http服务器提供服务,并且修改配置文件后也无需重启服务,支持动态加载。

我们新建一个文件data.json

[{ "request" : { "uri" : "/hello" }, "response" : { "text" : "Hello  World !!!" } } ]

使用cmd输入指令
java -jar moco-runner-0.11.0-standalone.jar start -p 5638 -c data.json
这个命令的意思是要mock监听本地的5638端口,对应的请求返回的数据在我们的data.json里面。
当有网络请求到来的时候,我们的mock就会去查data.json,把request为hello对应的数据返回,

回车后会有如下信息,表示开启成功
INFO Server is started at 5638
INFO Shutdown port is 52384
在这里插入图片描述
打开浏览器输入http://localhost:5638/hello

在这里插入图片描述

我们也可以写一些复杂的请求

{ 
 "request" : { 
    "uri" : "/user/getUser",
     "queries": { "name":"jack" } 
  },
  "response" : {
       "text" : "Hey. I'm jack" 
     }  
}

支持正则表达式

{ 
  "request": { 
       "uri": { 
       "match": "/getUser/\\w+" } 
    },
  "response": {
       "text": "Find a boy."
   } 
}

支持POST , GET , PUT , DELETE 等方法

{
  "request" :{
      "method" : "post",
       "uri": "/getUser",
      "forms" :{
          "name" : "jack"
       }
    },
  "response" : {
      "text" : "Hi. you get jack data"
    }
} 

我们可以用postman来发起请求,来测试数据正确
在这里插入图片描述
支持特定的头的和cookies

{
  "request" :{
      "method" : "post",
      "headers" : {
        "content-type" : "application/json"
      }
    },
  "response" :{
      "status" : 200,
      "text" : "bar",
       "cookies" :{
          "login" : "true"
      },
       "headers" :{
          "content-type" : "application/json"
       }
    }
}

延迟功能,下面延迟1s响应

{
  "request" :{
      "text" : "foo"
    },
  "response" :{
      "latency": {
          "duration": 1,
          "unit": "second"
        }
    }
}

上面的请求参数的值和返回值都是固定的,这自然太过死板。
好在从0.8版本开始,Moco提供了template功能,可以动态的返回一些参数值
例如:

  {
    "request": {
        "uri": "/getUser2"
        },
    "response": {
          "text": {
             "template": "${req.queries['name']}"
                 }
        }
 }

这样,但当我们的请求是localhost:5638/getUser2?name=nameIsJack,那么返回的数据是nameIsJack

还有的模板有:

"template": "${req.version}"
"template": "${req.method}"
"template": "${req.content}"
"template": "${req.headers['foo']}"
"template": "${req.queries['foo']}"
"template": "${req.forms['foo']}"
"template": "${req.cookies['foo']}"

从0.9版本开始,mock提供了event方法,什么意思呢
有时候,我们请求一些特定接口的时候,我们可能需要去请求别的地址,从而才能完成请求。
例如OAuth等,牵涉到第三方的情况。这时候,Event就派上大用场了

 {
    "request": {
        "uri" : "/event"
    },
    "response": {
        "text": "event"
    },
    "on": {
        "complete": {
            "get" : {
                "url" : "http://another_site/OAuth?xxx=xxxx"
            }
        }
    }
}

这样我们就可以等去验证完权限部分,才返回结果。

异步请求 Asynchronous
前面的请求默认都是同步的,这意味着只有等到服务器处理完,结果才会返回给客户端
如果你的实际情况不是这样,需要来点异步的,那么从0.9版本开始,有这个功能了,另外你还可以延迟个5秒,像这样

{
        "request": {
            "uri" : "/event"
        },
        "response": {
            "text": "event"
        },
        "on": {
            "complete": {
            "async" : "true",
            "latency" : 5000,
                "post" : {
                    "url" : "http://www.baidu.com",
                    "content": "content"
                }
            }
        }
    }

在线的mock服务器

https://getman.cn/mock

先写接口
在这里插入图片描述
去测试
在这里插入图片描述

Mockito框架

摸了,待更

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值