浅析单元测试

49 阅读5分钟

一、什么是单元测试

在测试领域,我们有定义较多专业术语:单元测试、增量测试、集成测试、回归测试、冒烟测试。谷歌看到这种“百家争鸣”的现象,创立了自己的命名方式,只分为小型测试中型测试大型测试

  • 小型测试,针对单个函数的测试,关注其内部逻辑,mock所有需要的服务。小型测试带来优秀的代码质量、良好的异常处理、优雅的错误报告。小型测试对应测试类型即是单元测试。
  • 中型测试,验证两个或多个制定的模块应用之间的交互
  • 大型测试,也被称为“系统测试”或“端到端测试”。大型测试在一个较高层次上运行,验证系统作为一个整体是如何工作的。
资源小型测试中型测试大型测试
网络访问仅访问localhost
数据库访问
访问文件
访问用户界面
使用外部服务不鼓励,可mock
多线程
使用sleep语句
使用系统属性设置
运行时间限制(毫秒)60300900+
强制时间限制(分钟)1515
小型测试中型测试大型测试
对应测试类型单元测试单元测试+逻辑层测试(泛单元或分层测试)UI测试或接口测试

我们的单元测试,既可以是针对一个函数写case,也可以按照函数的调用关系串起来写case。

二、测试模型中的单元测试定位

冰淇淋模型

冰淇淋模型:主要依靠端对端测试,来保障软件的正确性

优点: 端到端测试模拟的用户实际行为,测试的是系统整体,更有能力发现全局,测试信心更强。

缺点: 但是随着产品壮大,手工回归测试时间越来越长,质量很难把控;自动化case频频失败,每一个失败对应着一个长长的函数调用,难以定位问题;单元测试极少,基本没作用。

金字塔模型

金字塔模型:在单元测试,集成测试和端到端测试三个阶段的测试资源投入,应该满足接近7:2:1的关系,通过测试左移和质量内建,从源头上保障软件质量,实现预防bug(而非发现bug)的目标。

优点: 大部分问题消灭在单元测试阶段,可把有限的资源投入到主干和核心端到端测试上面。

缺点: 测试的对象是代码,离用户实际操作还是有距离,可能存在一定风险,进而影响我们降低对软件质量的信心。

三、为什么做单元测试?

工程师对待单元测试的普遍想法:

  • 单元测试浪费了太多的时间
  • 单元测试仅仅是证明这些代码做了什么
  • 我是很棒的程序员,我是不是可以不进行单元测试?
  • 后面的集成测试将会抓住所有的bug
  • 单元测试的成本效率不高我把测试都写了,那么测试人员做什么呢?
  • 公司请我来是写代码,而不是写测试
  • 测试代码的正确性,并不是我的工作

稍安勿躁,我们先通过业内一些数据,全局了解单元测试优势:

处理bug时间成本考量,bug在单元测试阶段被发现,平均耗时3.25小时,如果漏到系统测试阶段,要花费11.5小时。

85%的缺陷都在代码设计阶段产生,而发现bug的阶段越靠后,耗费成本就越高,指数级别的增高。

在《单元测试的艺术》这本书提到一个案例:找了开发能力相近的两个团队,同时开发相近的需求。

进行单测的团队在编码阶段时长增长了一倍,从7天到14天,但是,这个团队在集成测试阶段的表现非常顺畅,bug量小,定位bug迅速等。最终的效果,整体交付时间和缺陷数,均是单测团队最少。

四、怎么做单元测试?

这里选择PHP的Laravel框架,框架有集成phpunit测试组件,可以让我们快速开展单元测试~

产品功能:

线上商品对应有 id 和 刊登笔数nums,在活动周期内,可以在原有的刊登笔数之外获得“额外的赠送”

创建商品类 APP/Product.php

<?php

namespace App;

class Product
{
    protected $id;
    protected $nums;

    public function __construct($id, $nums)
    {
        $this->id = $id;
        $this->nums = $nums;
    }

    public function id()
    {
        return $this->id;
    }

    public function nums()
    {
        return $this->nums;
    }
}

创建活动类 APP/Activity.php

<?php

namespace App;

class Activity
{
    protected $product;
    protected $activityGoods;

    public function __construct(Product $product)
    {
        $this->product = $product;
        //参与本次活动的商品id
        $this->activityGoods = ['1','2','3'];
    }

    /**
     * 判断此商品方案是否可参与活动
     */
    public function isActivityGoods()
    {
        return in_array($this->product->id(), $this->activityGoods);
    }

    /**
     * 商品赠送:每满5个送7个,每满2个送2个
     */
    public function getGiftStandNums($buyNums)
    {
        $giftStandNums = 0;

        $numDishesOfFive = floor($buyNums / 5);
        $giftStandNums += $numDishesOfFive * 7;
        $buyNums -= $numDishesOfFive * 5;

        $numDishesOfTwo = floor($buyNums / 2);
        $giftStandNums += $numDishesOfTwo * 2;

        return $giftStandNums ? intval($giftStandNums) : 0;
    }
}

接着,我们对活动进行单元测试,验证两个活动函数是否正确

Step1:生成单元测试类

执行命令:php artisan make:test ActivityTest --unit

Step2:实现活动测试类

<?php

namespace Tests\Unit;

use App\Product;
use App\Activity;
use Tests\TestCase;

class ActivityTest extends TestCase
{
    /**
     * 判断此商品方案是否可参与活动
     */
    public function testIsActivityGoods()
    {
        $product = new Product(1, 4);
        $activity = new Activity($product);
        $this->assertTrue($activity->isActivityGoods());
    }

    /**
     * 判断此额外赠送个数是否正确
     */
    public function testGetGiftStandNums()
    {
        $product = new Product(1, 4);
        $activity = new Activity($product);
        $this->assertEquals(4, $activity->getGiftStandNums($product->nums));
    }
}

Step3:执行单元测试,显示通过

命令:phpunit --filter Activity

以上活动测试类,仅算做了,有两项重点,等待我们进一步优化!

  • 存在冗余且重复代码违反DRY设计原则,可封装到基境Setup()测试类方法中
  • 可以绑定测试数据集@dataProvider additionProvider,便于后续回归测试

优化后的活动测试类如下:

<?php

namespace Tests\Unit;

use App\Product;
use App\Activity;
use Tests\TestCase;

class ActivityTest extends TestCase
{
    protected $product;
    protected $activity;

    /**
     * fixture 基境
     * @return void
     */
    public function setUp(): void
    {
        $this->product = new Product(1, 2);
        $this->activity = new Activity($this->product);
    }

    /**
     * 判断此商品方案是否可参与活动
     */
    public function testIsActivityGoods()
    {
        $this->assertTrue($this->activity->isActivityGoods());
    }

    /**
     * 判断此额外赠送个数是否正确
     * @dataProvider additionProvider
     */
    public function testGetGiftStandNums($input, $output)
    {
        $this->assertEquals($output, $this->activity->getGiftStandNums($input));
    }

    public function additionProvider()
    {
        return [
            [2, 2],
            [4, 4],
            [5, 7],
            [6, 7]
        ];
    }
}

相关phpunit扩展,可查阅手册

面对更为复杂的业务场景,也可以借助模型工厂、Fixture(基境)、以及Mock来实现单元测试。

五、总结

本文从单元测试概念入手,讨论了单元测试定位与价值,并用一则phpunit案例入门了单元测试。

单元测试具有实现成本低和执行速度快的特点,因而可以很容易地与持续集成和敏捷开发相结合,共同实现软件的快速迭代。同时,好的单元测试又可以转化为回归测试一部分,在快速迭代开发中保障质量。

参考资料:

初识phpunit

phpunit手册

Testing Laravel 单元测试入门笔记

测试策略模型有哪些

从头到脚说单测——谈有效的单元测试