一、什么是单元测试
在测试领域,我们有定义较多专业术语:单元测试、增量测试、集成测试、回归测试、冒烟测试。谷歌看到这种“百家争鸣”的现象,创立了自己的命名方式,只分为小型测试、中型测试和大型测试。
- 小型测试,针对单个函数的测试,关注其内部逻辑,mock所有需要的服务。小型测试带来优秀的代码质量、良好的异常处理、优雅的错误报告。小型测试对应测试类型即是单元测试。
- 中型测试,验证两个或多个制定的模块应用之间的交互
- 大型测试,也被称为“系统测试”或“端到端测试”。大型测试在一个较高层次上运行,验证系统作为一个整体是如何工作的。
资源 | 小型测试 | 中型测试 | 大型测试 |
---|---|---|---|
网络访问 | 否 | 仅访问localhost | 是 |
数据库访问 | 否 | 是 | 是 |
访问文件 | 否 | 是 | 是 |
访问用户界面 | 否 | 否 | 是 |
使用外部服务 | 否 | 不鼓励,可mock | 是 |
多线程 | 否 | 是 | 是 |
使用sleep语句 | 否 | 是 | 是 |
使用系统属性设置 | 否 | 是 | 是 |
运行时间限制(毫秒) | 60 | 300 | 900+ |
强制时间限制(分钟) | 1 | 5 | 15 |
小型测试 | 中型测试 | 大型测试 | |
---|---|---|---|
对应测试类型 | 单元测试 | 单元测试+逻辑层测试(泛单元或分层测试) | 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案例入门了单元测试。
单元测试具有实现成本低和执行速度快的特点,因而可以很容易地与持续集成和敏捷开发相结合,共同实现软件的快速迭代。同时,好的单元测试又可以转化为回归测试一部分,在快速迭代开发中保障质量。
参考资料: