Codeception 测试 Php 代码

Codeception 测试 Php 代码

一、一句话概述

使用 cc 进行单元测试,保证现有代码质量,为以后维护与重构提供支撑。

二、目标

  • 安装配置 cc
  • 编写测试代码,简化开发与最大化稳定性和可维护性

三、测试的类型

1. 单元测试(UT)

  • 执行一段与其他代码完全隔离的代码单元
  • 断言代码行为
  • 描述用例的预期

2. 功能测试(FT)

  • 执行应用(客户端)请求
  • 断言返回值
  • 描述应用预期行为
  • 依赖框架

3. Web 服务测试

  • 通过 http client 执行 api 请求
  • 对 api 返回值断言
  • 描述 api 行为

四、系统要求

  • PHP 5.4+
  • json 扩展
  • mbstring 扩展
  • xdebug 扩展(生成覆盖率报告)

下载安装配置 php扩展 xdebug

版本选用最新稳定版: 2.6.1

1
2
3
4
5
6
7
8
9
10
11
12
13
wget https://pecl.php.net/get/xdebug-2.6.1.tgz
tar xvf xdebug-2.6.1.tgz
cd xdebug-2.6.1
/path/to/phpize
./configure --enable-xdebug
make
make install

在 php.ini 文件中添加此行:
zend_extension="/wherever/you/put/it/xdebug.so"

查看是否安装上:
php -i | grep xdebug

五、安装配置与运行 Codeception

1
2
3
4
5
6
7
8
9
wget http://codeception.com/codecept.phar # 不需要解压

php codecept.phar help # 帮助文档
php codecept.phar bootstrap # 创建 test & codeception.yml 到当前文件夹
php codecept.phar build # 通过配置生成必要的类,每次修改配置都需要运行次命令

php codecept.phar run {suite_name} # 不跟 suite name 即是运行全部
php codecept.phar run api ThingTest.php
php codecept.phar run api ThingTest.php:method

配置与代码示例

地址:https://github.com/wdy1184/CodeceptionExampleWithYii2

六、测试代码格式

codeception 提供了两种写测试代码的方式

  1. Cept: 一个独立的测试文件。可以写一套测试流程。
  2. Cest: 一个类文件,多个测试写在一个类中。
  3. PHPUnit: 支持 phpunit 原生测试框架的格式

七、与Yii2集成

配置:

image-20190305200555502.png

_bootstrap.php 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

defined('APP_PATH') or define('APP_PATH', dirname(dirname(__FILE__)));
// 注册 Composer 自动加载器
// 包含 Yii 类文件

require(__DIR__ . '/../../psservice/libs/envFun.php');
require(__DIR__ . '/../../vendor/autoload.php');

// 环境变量配置
defined('YII_DEBUG') or define('YII_DEBUG', getYaconfEnv('YII_DEBUG'));
defined('YII_ENV') or define('YII_ENV', getYaconfEnv('YII_ENV'));
defined('YII_ENABLE_ERROR_HANDLER') or define('YII_ENABLE_ERROR_HANDLER', getYaconfEnv('YII_ENABLE_ERROR_HANDLER'));

require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');

// 暂时向下兼容老文件
require(__DIR__ . '/../../config/errorCode.php');
require(__DIR__ . '/../../config/constant.php');
Yii::setAlias('@backend', dirname(dirname(__DIR__)) . '/backend');
Yii::setAlias('@console', dirname(dirname(__DIR__)) . '/console');
Yii::setAlias('@psservice', dirname(dirname(__DIR__)) . '/psservice');
Yii::setAlias('@config', dirname(dirname(__DIR__)) . '/config');

$config = require(APP_PATH . '/../config/web.php');

Cest 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<?php
namespace tests\api;

use backend\models\user\EduOcrTourist;
use backend\models\user\EduOcrUser;
use backend\modules\user\services\UserEntity;
use config\UserCacheKey;
use psservice\models\user\WxEduUser;
use tests\ApiTester;

class UserControllerCest
{
private $token;

public function _before(ApiTester $I)
{
}

/**
* 用户登录
* @param ApiTester $I
*/
public function actionLoginTest(ApiTester $I)
{
$mobile = '18510473853';
$token = $I->amUserLogin($mobile);
$I->sendCommandToRedis('select', getPsYaconfEnv('PS_OCR_SINGLE_REDIS_DATABASE_1'));

$userRedisInfo = $I->grabFromRedis(sprintf(UserCacheKey::USER_INFO, $token));
$userRedisInfo = json_decode($userRedisInfo, 1);
codecept_debug($userRedisInfo);

$I->assertGreaterThan(0, $userRedisInfo['userId']);
$I->assertEquals(0, $userRedisInfo['touristUserId']);
$I->assertEquals(UserEntity::$roleMap['user'], $userRedisInfo['role']);
$I->assertEquals($mobile, $userRedisInfo['mobile']);

$I->assertLessOrEquals(10, (time() - strtotime($userRedisInfo['loginTime'])));

$I->seeRecord(WxEduUser::className(), ['UserId' => $userRedisInfo['commonUserId']]);
}

/**
* 游客登录
* @param ApiTester $I
*/
public function actionTouristLoginTest(ApiTester $I)
{
$token = $I->amTouristLogin('6CB7B05E-DE62-415A-96CF-6D57B666F939');
$I->sendCommandToRedis('select', getPsYaconfEnv('PS_OCR_SINGLE_REDIS_DATABASE_1'));

// $userRedisInfo = $I->grabFromRedis(sprintf(UserCacheKey::TOURIST_INFO, $token)); // 机缘巧合下,没有使用 touristInfo 常量
$userRedisInfo = $I->grabFromRedis(sprintf(UserCacheKey::USER_INFO, $token));
$userRedisInfo = json_decode($userRedisInfo, 1);
codecept_debug($userRedisInfo);

$I->assertGreaterThan(0, $userRedisInfo['id']);


$I->seeRecord(EduOcrTourist::className(), ['deviceId' => '6CB7B05E-DE62-415A-96CF-6D57B666F939']);
}

/**
* 登出
* @param ApiTester $I
*/
public function actionLogoutTest(ApiTester $I)
{
$this->token = $I->amUserLogin('18510473853');
$I->sendPOST('/user/user/logout', [
'token' => $this->token,
'deviceId' => '6CB7B05E-DE62-415A-96CF-6D57B666F939',
]);

$I->seeResponseContainsJson(['code' => 99999]);
}

/**
* 登录用户信息
*/
public function actionInfoTest(ApiTester $I)
{
$mobile = '18510473853';
$this->token = $I->amUserLogin($mobile);
$I->sendGET('/user/user/info', [
'token' => $this->token,
]);
$ret = json_decode($I->grabResponse(), 1);
$I->seeRecord(EduOcrUser::className(), ['id' => $ret['data']['userId'], 'mobile' => $mobile]);
}
}

八、增加自定义方法

tests/_support/Helper/{suite_name}.php

在其中增加方法后,可以通过 ApiTester 的对象 $I 进行调用。

image-20190305201253542.png

九、运行测试代码生成测试报告(代码覆盖率)

1
php codecept.phar run -coverage --coverage-html --coverage-xml

测试报告会在 tests/_output/coverage/ 中生成。

通过设定 nginx 静态服务器,可以查看测试报告。如:

image-20190305204312240.png

十、利用测试用例进行重构

步骤:

  1. 编写测试代码
  2. 持续维护测试代码,提高测试代码覆盖率
  3. 运行测试代码使其通过。并不断重复 1、2、3 步骤
  4. 代码库越来越庞杂
  5. 重构
    1. 合并相似循环、方法、类
    2. 提取方法
    3. 等等
  6. 运行测试用例,找到失败的用例
  7. 修改重构后的代码,使其通过测试用例,并重复 5、6、7 步骤

十一、利用测试用例,快速了解新代码

steps 会在执行的时候打印出执行的断言,有助于观察测试用例做了什么,以及怎么使用你写的接口。

即测试用例是 : self-document 的。

1
php codecept.phar run api --steps # steps 会在执行的时候打印出执行的断言

image-20190305202846264.png