浅谈单元测试

单元测试

软件测试按照阶段可分为单元测试、集成测试、系统测试以及验收测试,今天我们要介绍的就是单元测试。

阶段 测试对象 测试人员 测试方法
单元测试 编码后 最小单位程序模块 软件开发人员 白盒测试
集成测试 单元测试之后 组装后的模块 软件开发人员 灰盒测试
系统测试 集成测试之后 已经集成好的软件系统 测试人员 黑盒测试
验收测试 系统测试之后 整个系统 测试人员 黑盒测试

1、什么是单元测试?

首先我们要先了解一下什么是单元,单元就是指人为规定的最小的被测功能模块,在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。而单元测试则是对这些 ” 单元 “ 进行检测,看功能是否正确。单元测试的编写人员是软件开发人员。

2、单元测试的作用和局限性有哪些呢?

从测试金字塔中我们可以发现,越是底层的测试,牵扯到相关内容越少,而高层测试则涉及面更广,因此bug发现的越晚,修改的成本就越高。比如单元测试,它的关注点只有一个单元,而没有其它任何东西。所以单元测试修改bug的成本是最小的。进行单元测试具有一下作用:

  • 提高代码质量

  • 提升开发效率

  • 降低开发成本

单元测试的局限性:

  • 单元测试只测试程序单元自身的功能。因此,它不能发现集成错误、性能问题、或者其他系统级别的问题。

3、好的单元测试时什么样的?

  • 正确清晰:有利于帮助其他开发者理解代码逻辑,理解如何使⽤相关的类或者函数。

  • 完整:良好设计的单元测试案例覆盖程序单元分支和循环条件的所有路径。好的单元测试完备⽽不重复。同样的测试场景,或者同类型的测试输⼊不要写多个单元测试,找⼀个有代表性的场景输⼊就可以了。

  • 健壮:当被测试的类或者函数被修改内部实现或者添加功能时,⼀个好的单测应该完全不需要被修改或者只有极少的修改。

4、单元测试的步骤

单元测试的代码结构⼀般一个三步经典结构:准备,调⽤,断⾔。

  • 准备:⽬的是准备好调⽤所需要的外部环境,如数据,Stub,Mock,临时变量,调⽤请求,环境背景变量等等。

  • 调⽤:实际调⽤需要测试⽅法,函数或者流程。

  • 断⾔:判断调⽤部分的返回结果是否符合预期。

测试框架JUnit

JUnit是用于编写可复用测试集的简单框架,是xUnit的一个子集。xUnit是一套基于测试驱动开发的测试框架,有PythonUnit、CppUnit、JUnit等。

1、常用注解的使用

注解 说明
@Before 初始化方法
@After 释放资源
@Test 测试方法,在这里可以测试期望异常和超时时间
@Ignore 忽略的测试方法
@BeforeClass 针对所有测试,只执行一次,且必须为static void
@AfterClass 针对所有测试,只执行一次,且必须为static void
@RunWith 指定测试类使用某个运行器
@Parameters 指定测试类的测试数据集合
@Rule 允许灵活添加或重新定义测试类中的每个测试方法的行为
@FixMethodOrder 指定测试方法的执行顺序
public class ClassNameTest {
    @BeforeClass
    public static void beforeClass() throws Exception {
        System.out.println("测试类执行之前执行,主要用来初使化公共资源等");
    }

    @AfterClass
    public static void afterClass() throws Exception {
        System.out.println("测试类执行之后执行,主要用来释放资源或清理工作");
    }

    @Before
    public void setup() throws Exception {
        System.out.println("测试方法执行之前执行");
    }

    @After
    public void teardown() throws Exception {
        System.out.println("测试方法执行之后执行");
    }

    @Test
    public void test() {
        System.out.println("测试方法");
    }

    @Test
    @Ignore("可以忽略这个方法")
    public void testIgnore() {
        System.out.println("测试忽略方法");
    }

    @Test(expected = ArithmeticException.class)
    public void divisionWithException() {
        System.out.println("测试异常");
        int i = 1 / 0;
    }

    @Test(timeout = 1000)
    public void infinity() {
        System.out.println("测试超时");
        while (true) ;
    }
}

2、常用断言

断言 说明
assertArrayEquals(expecteds, actuals) 查看两个数组是否相等。
assertEquals(expected, actual) 查看两个对象是否相等。类似于字符串比较使用的equals()方法
assertNotEquals(first, second) 查看两个对象是否不相等。
assertNull(object) 查看对象是否为空。
assertNotNull(object) 查看对象是否不为空。
assertSame(expected, actual) 查看两个对象的引用是否相等。类似于使用“==”比较两个对象
assertNotSame(unexpected, actual) 查看两个对象的引用是否不相等。类似于使用“!=”比较两个对象
assertTrue(condition) 查看运行结果是否为true。
assertFalse(condition) 查看运行结果是否为false。
assertThat(actual, matcher) 查看实际值是否满足指定的条件
fail() 让测试失败

Mockit模拟测试框架

Mockito 是一个强大的用于 Java 开发的模拟测试框架, 通过 Mockito 我们可以创建和配置 Mock 对象, 进而简化有外部依赖的类的测试.

1、Mockito框架的好处

  • 可以很简单的虚拟出一个复杂对象(比如虚拟出一个接口的实现类);

  • 可以配置 mock 对象的行为;

  • 可以使测试用例只注重测试流程与结果;

  • 减少外部类、系统和依赖给单元测试带来的耦合。

2、使用 Mockito 的大致流程如下

  • 创建外部依赖的 Mock 对象, 然后将此 Mock 对象注入到测试类中.

  • 执行测试代码.

  • 校验测试代码是否执行正确

3、什么是mock?

mock就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法,这个虚拟的对象就是mock对象。mock对象就是真实对象在调试期间的代替品。

如图所示,ConsumableQueryService耗材查询服务依赖ConsumableTransfer和ConsumableRemoteService,现在需要测试耗材查询服务,一种方法是构建真实的 ConsumableTransfer,和ConsumableRemoteService 实例, 然后注入到 ConsumableQueryService 中,这样就违背了单元测试只测试程序单元自身的功能的原则,这时候我们就需要使用mock对象来进行替代了。替换后, 我们就可以对 ConsumableQueryService 进行测试, 并且不需要关注它的复杂的依赖。

4、Mockito使用

使用MockitoAnnotations模拟对象

public class ConsumableQueryServiceTest {
    @InjectMocks
    private ConsumableQueryService consumableQueryService;

    @Mock
    private ConsumableTransfer consumableTransfer;

    @Mock
    private ConsumableRemoteService consumableRemoteService;

    @Before
    public void initMock() {
        MockitoAnnotations.initMocks(this);
        ConsumableBatchesQO qo = new ConsumableBatchesQO();
        qo.setChannelId(1);
        qo.setWarehouseId(1);
        qo.setKeyword("耗材");
        qo.setCascaded(true);
        qo.setNeedOutOfDate(Boolean.FALSE);
        List<ConsumableBatchesInfo> consumableBatchesInfos = new ArrayList<>();
        ConsumableBatchesInfo batchesInfo = new ConsumableBatchesInfo();
        batchesInfo.setUnitId(-1);
        batchesInfo.setName("耗材测试");
        ConsumableClinicInfo channelInfo = new ConsumableClinicInfo();
        batchesInfo.setChannelInfo(channelInfo);
        consumableBatchesInfos.add(batchesInfo);
        when(consumableRemoteService.findBatches(qo)).thenReturn(consumableBatchesInfos);
        ConsumableBO consumableBO = new ConsumableBO();
        consumableBO.setId(batchesInfo.getUnitId());
        consumableBO.setName(batchesInfo.getName());
        when(consumableTransfer.toConsumableBO(consumableBatchesInfos.get(0))).thenReturn(consumableBO);
    }

    @Test
    public void testQueryByKeyWord() {
        List<ConsumableBO> consumables = consumableQueryService.queryByKeyWord(1, 1, "耗材");
        Assert.assertEquals(consumables.get(0).getName(), "耗材测试");
    }
}

使用MockitoJUnitRunner模拟对象

@RunWith(MockitoJUnitRunner.class)
public class ConsumableQueryServiceTest {
    @InjectMocks
    private ConsumableQueryService consumableQueryService;

    @Mock
    private ConsumableTransfer consumableTransfer;

    @Mock
    private ConsumableRemoteService consumableRemoteService;

    @Before
    public void initMock() {
        ConsumableBatchesQO qo = new ConsumableBatchesQO();
        qo.setChannelId(1);
        qo.setWarehouseId(1);
        qo.setKeyword("耗材");
        qo.setCascaded(true);
        qo.setNeedOutOfDate(Boolean.FALSE);
        List<ConsumableBatchesInfo> consumableBatchesInfos = new ArrayList<>();
        ConsumableBatchesInfo batchesInfo = new ConsumableBatchesInfo();
        batchesInfo.setUnitId(-1);
        batchesInfo.setName("耗材测试");
        ConsumableClinicInfo channelInfo = new ConsumableClinicInfo();
        batchesInfo.setChannelInfo(channelInfo);
        consumableBatchesInfos.add(batchesInfo);
        when(consumableRemoteService.findBatches(qo)).thenReturn(consumableBatchesInfos);
        ConsumableBO consumableBO = new ConsumableBO();
        consumableBO.setId(batchesInfo.getUnitId());
        consumableBO.setName(batchesInfo.getName());
        when(consumableTransfer.toConsumableBO(consumableBatchesInfos.get(0))).thenReturn(consumableBO);
    }

    @Test
    public void testQueryByKeyWord() {
        List<ConsumableBO> consumables = consumableQueryService.queryByKeyWord(1, 1, "耗材");
        Assert.assertEquals(consumables.get(0).getName(), "耗材测试");
    }
}

使用MockitoRule模拟对象

public class ConsumableQueryServiceTest {
    @InjectMocks
    private ConsumableQueryService consumableQueryService;

    @Mock
    private ConsumableTransfer consumableTransfer;

    @Mock
    private ConsumableRemoteService consumableRemoteService;

    @Rule
    public MockitoRule rule = MockitoJUnit.rule();

    @Before
    public void initMock() {
        ConsumableBatchesQO qo = new ConsumableBatchesQO();
        qo.setChannelId(1);
        qo.setWarehouseId(1);
        qo.setKeyword("耗材");
        qo.setCascaded(true);
        qo.setNeedOutOfDate(Boolean.FALSE);
        List<ConsumableBatchesInfo> consumableBatchesInfos = new ArrayList<>();
        ConsumableBatchesInfo batchesInfo = new ConsumableBatchesInfo();
        batchesInfo.setUnitId(-1);
        batchesInfo.setName("耗材测试");
        ConsumableClinicInfo channelInfo = new ConsumableClinicInfo();
        batchesInfo.setChannelInfo(channelInfo);
        consumableBatchesInfos.add(batchesInfo);
        when(consumableRemoteService.findBatches(qo)).thenReturn(consumableBatchesInfos);
        ConsumableBO consumableBO = new ConsumableBO();
        consumableBO.setId(batchesInfo.getUnitId());
        consumableBO.setName(batchesInfo.getName());
        when(consumableTransfer.toConsumableBO(consumableBatchesInfos.get(0))).thenReturn(consumableBO);
    }

    @Test
    public void testQueryByKeyWord() {
        List<ConsumableBO> consumables = consumableQueryService.queryByKeyWord(1, 1, "耗材");
        Assert.assertEquals(consumables.get(0).getName(), "耗材测试");
    }
}

5、Mockit常用注解

  • @InjectMocks进行依赖注入

  • @Mock注解,我们用来初始化Mock对象

  • @Captor参数捕获器的注解

  • @Spy包装Java对象

6、Mockit常用方法

verify函数

verify函数默认验证的是执行了times(1),也就是某个测试函数是否执行了1次.因此,times(1)通常被省略了。

Method Meaning
times(n) 次数为n,默认为1(times(1))
never() 次数为0,相当于times(0)
atLeast(n) 最少n次
atLeastOnce() 最少一次
atMost(n) 最多n次

实列化虚拟对象

@Before
    public void initMock() {
        ConsumableBatchesQO qo = new ConsumableBatchesQO();
        qo.setChannelId(1);
        qo.setWarehouseId(1);
        qo.setKeyword("耗材");
        qo.setCascaded(true);
        qo.setNeedOutOfDate(Boolean.FALSE);
        List<ConsumableBatchesInfo> consumableBatchesInfos = new ArrayList<>();
        ConsumableBatchesInfo batchesInfo = new ConsumableBatchesInfo();
        batchesInfo.setUnitId(-1);
        batchesInfo.setName("耗材测试");
        ConsumableClinicInfo channelInfo = new ConsumableClinicInfo();
        batchesInfo.setChannelInfo(channelInfo);
        consumableBatchesInfos.add(batchesInfo);
        //实例化虚拟对象
        when(consumableRemoteService.findBatches(qo)).thenReturn(consumableBatchesInfos);
        consumableRemoteService.findBatches(qo);
        consumableRemoteService.findBatches(qo);
        //校验次数
        verify(consumableRemoteService, times(2)).findBatches(qo);
        ConsumableBO consumableBO = new ConsumableBO();
        consumableBO.setId(batchesInfo.getUnitId());
        consumableBO.setName(batchesInfo.getName());
        when(consumableTransfer.toConsumableBO(consumableBatchesInfos.get(0))).thenReturn(consumableBO);
    }
posted @ 2020-04-02 18:56  AmyZheng  阅读(355)  评论(0编辑  收藏  举报