测试的运行流程
最后更新于:2022-04-02 00:33:47
# 流程
1. 使用visit或者是submitForm发起请求
2. request::create构建请求
3. 使用App::run执行请求指定的操作
4. 操作执行完毕回显结果
5. App::run返回response
6. 使用see等等对执行结果进行断言
# 从App类讲起
项目的入口文件是index.php,该文件会调用thinkphp目录下的start.php。而start.php主要的两个作用就是导入base.php文件和运行app。
`App::run()->send();`
App是thinkPHP/library/think目录下的一个类。从该文件中我们可以知道,run会接受一个参数(request)。当这个参数为空的时候TP就会解析实际上的请求,当这个参数不为空的时候,TP实际上解析的便是这个参数。
# request支持构建请求
在request中有一个方法create,这个方法传入相关的参数便可以构建一个请求。其返回值便是一个request,我们再将这个request传递给App运行即可。
`$response = App::run($request);`
这个response便是处理的结果,这个结果将会跟网页显示的内容的一模一样的,因此我们对这个结果进行断言,便可以完成对整个单元测试流程了。
';
原理
最后更新于:2022-04-02 00:33:45
[测试的运行流程](3.%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90/1.%E6%B5%8B%E8%AF%95%E7%9A%84%E8%BF%90%E8%A1%8C%E6%B5%81%E7%A8%8B.md)
';
使用PHPUNIT断言
最后更新于:2022-04-02 00:33:43
使用测试套件
最后更新于:2022-04-02 00:33:40
# 使用测试套件
## 功能测试
由于thinkphp的测试扩展是基于phpunit的,因此phpunit的所有功能,在thinkphp的测试扩展中也能够使用。
当项目开始变得复杂,有多个模块的时候,我们在开发某个功能的时候,通常希望只测试某个功能。
这个时候,我们就能够使用phpunit的命令选项:--filter了。
一些常见的过滤例子为:
```
--filter 'TestNamespace\\TestCaseClass::testMethod'
--filter 'TestNamespace\\TestCaseClass'
--filter TestNamespace
--filter TestCaseClass
--filter testMethod
--filter '/::testMethod .*"my named data"/'
--filter '/::testMethod .*#5$/'
--filter '/::testMethod .*#(5|6|7)$/'
```
更多内容请参考[phpunit](http://www.phpunit.cn/manual/current/zh_cn/textui.html#textui.clioptions)
## 套件测试
当项目有多个模块组成的时候,我们通常希望测试速度够快,又不用测试整个项目。这个时候,我们就可以使用phpunit的套件功能了。
测试套件有两种方式组织,分别为通过目录和通过xml组织,在这里推荐使用第一种。
有多少个功能模块的时候,就在tests目录下新建多少个目录,然后在测试的时候,使用以下命令:
`php think unit tests/xxx`
通过这种方式,我们就能单独测试某个子目录下的测试了,这种也称之为测试套件。
';
对数据库进行断言
最后更新于:2022-04-02 00:33:38
由于TP测试框架更偏向于黑盒测试(实际上我们使用的是灰盒测试),因此在对数据库进行测试的时候,我们没有办法(或者是不方便)使用mock。所以我们使用TP测试框架的时候对数据操作进行测试的时候,我们需要为数据库的基境进行搭建。
> 1. 黑盒测试:测试人员不需要了解代码如何工作,只需要根据指定输入,断言输出。
> 2. 白盒测试:测试人员需要了解代码,用来帮助测试的进行。
> 3. 灰盒测试:黑盒和白盒的中间,使用黑盒方法的同时使用白盒测试。
TP的官方测试框架主要使用灰盒的测试方法,如果需要使用mock的话,可以使用快速入门中提到的测试库。
## 安装DBUnit。
使用thinkUnit的时候安装的PHPUNIT依赖中是没有DBUNIT的。因此如果我们要对数据库进行测试的话,那么就需要安装DBUnit了。
可以在composer中的require-dev中添加以下的依赖:
`"phpunit/dbunit": ">=1.2"`
然后更新composer即可。
`composer update`
## 编写测试用例
安装完DBUnit之后,我们就需要重新编写测试用例了。不过,TP本身的测试库并没有提供数据库测试的测试用例基类,因此需要我们自己编写。
```
......其他配置
......
```
而后在getConnection方法为:
```
if ($this->conn === null) {
if (self::$pdo == null) {
self::$pdo = new \PDO( $GLOBALS['DB_DSN'], $GLOBALS['DB_USER'], $GLOBALS['DB_PASSWD'] );
}
$this->conn = $this->createDefaultDBConnection(self::$pdo, $GLOBALS['DB_DBNAME']);
}
return $this->conn;
```
> 再走一步:但是这样的话,我们每个方法也需要写这样重复的内容。因此,我们还可以将其抽象为父类,然后再需要使用数据库的测试类中继承该类。
';
对网页进行断言
最后更新于:2022-04-02 00:33:36
TP单元测试扩展中对网页元素的断言是基于symfony部分组件的,因此在使用对网页断言的时候还需要安装一个CssSelector的symfony组件。打开命令行,切换到TP的根目录下,运行下列的命令:
`composer require symfony/css-selector:2.8`
等待其安装后便可以了。下面我们使用几个例子来说明一下如何使用网页元素断言。
# seeInElement
断言某个元素内是否存在某个元素。
@param $element 指定的元素,如body
@param $text 要断言的文本
@param $negate 是否断言不存在,默认为false
假设有如下的网页:
~~~
Hello !
~~~
如果要断言字符串hello存在于body元素内,而字符串test不存在body里面,我们可以这么写:
~~~
visit('/index/index/index')->seeInElement('body','hello');
$this->visit('/index/index/index')->seeInElement('body','test',TRUE);
}
}
~~~
# notSeeInElement
效果如同seeInElement中将第三个参数设置为true
@param $element 指定的元素,如bod
@param $text 要断言的文本
例子如上。
# seeLink
断言某个链接是否存在,当第二个参数被设置时,同时断言其链接。
@param $text 某个a元素的文本,比如`test`,那么这里应该是test
@param $url 默认为null,即断言找到的链接其地址是否为该url
假设有如下的网页:
~~~
百度一下你就知道
~~~
如果要断言链接百度一下你就知道的a标签的地址,可以如下:
~~~
visit('/index/index/index')->seeLink('百度一下你就知道','http://www.baidu.com');
}
}
~~~
# notSeeLink
断言不存在某个链接。
@param $text 某个a元素的文本,比如`test`,那么这里应该是test
@param $url 默认为null,即断言找到的链接其地址是否为该url
例子如上。
# SeeInField
对input或者是textArea的value进行断言。
@param $selector name的值或者是id值
@param $expected 期望valued的值
如下网页:
~~~
~~~
我们即可以对name=test的input,也可以对id=myInput的input的value进行断言:
~~~
visit('/index/index/index')->seeInField('test','hello');//通过name定位元素
$this->visit('/index/index/index')->seeInField('myInput','hello');//通过id定位元素
}
}
~~~
# notSeeInField
对input或者是textArea的value进行断言。
@param $selector name的值或者是id值
@param $expected 期望valued的值
例子如上。
# seeIsChecked
对某个input选中进行断言。
# notSeeIsChecked
对某个input不是选中进行断言。
# seeIsSelected
对某个inpust选择进行断言。
# notSeeIsSelected
对某个input不选择进行断言。
# seePageIs
对某个页面的uri进行断言。
';
seeXX断言系列
最后更新于:2022-04-02 00:33:34
在TP单元测试扩展中,增加的断言最多便是seeXX断言系列。在快速入门的例子中,我们就使用了see断言函数。在扩展中,还有其他的see函数,他的名字通常是seeXX,比如seeJson。
# see
接收两个参数,用于断言一个正则是否在结果中可以匹配。第二个参数是布尔值,当为假的时候断言第一个参数在结果中匹配。当为真的时候断言第一个参数不在结果中匹配。
@param string $text 欲查找的文本
@param boolean $negate 非操作
# notSee
本方法使用了see,并且设置see的第二个参数为真。
@param string $text 欲查找的文本
# seeJson
内部使用seeJsonContains,用于断言某个json是否在结果中
@param array $data 期望的json
@param boolean $negate 非操作,为真时是期望$data不在结果中
# seeJsonEquals
断言json是否跟结果相等。
@param array $data 传入一个数组,排序再转换成json
其内部会将实际值转回成数组进行排序后再跟$data排序后转换成的json进行断言。
# seeJsonContains
断言某个json是否在结果中。
@param array $data 期望的与断言的json数据
@param boolean $negate 非操作
# seeModule
用于断言请求中的模块。
@param string @module 欲断言的模块名字
`$this->visit('/index/index/index')->seeModule('index');`
# seeController
用于断言请求中的控制器。
@param string @controller 欲断言的控制器名字
`$this->visit('/index/index/index')->seeController('Index');`
>注意,控制器首字母是需要大写的。
# seeAction
用于断言请求中的操作。
@param string @action 欲断言的操作名字
`$this->visit('/index/index/index')->seeAction('index');`
# seeStatusCode
用于断言response的状态码。
@param int $status 状态码
# seeHeader
用于断言response的head部。
@param string $headerName 头部的某个项
@param string $value 项值
# seeCookie
用于断言Cookie中是否存在某个值。
@param string $cookieName cookie的某个项名
@param string $value 项值
';
对模板变量进行断言
最后更新于:2022-04-02 00:33:31
在TP3.X中,我们通常使用$this->display()将一个模板渲染到网页上面去。比如在Index模块下面,我们新建一个view目录,然后再创建一个index目录,在其中创建一个html文件为index.html。如果要将这个html文件渲染到网页上面去的话,在TP5.X中,我们可以使用下面的语句:
`return view('index');`
如果我们要在一个网页中输出变量的话,我们可以在view助手函数的第二个参数中将值传入进去。
HTML网页:
~~~
Hello {$name}!
~~~
其对应的PHP代码:
~~~
'c7']);
}
}
~~~
那么如果我们要对上面的模板变量进行测试的话,我们可以使用下面三种方法:
1. assertViewHas
2. assertViewHasAll
3. assertViewMissing
第一个函数接受一个key值和一个value。
第二个函数则接受一个由key-value组成的数组。
第三个函数接受一个key,断言渲染的时候不存在某个key值的模板变量。
针对上面的例子,我们可以这么断言:
~~~
visit('/index/index/index')->assertViewHas('name','c7');
}
}
~~~
';
对响应状态断言
最后更新于:2022-04-02 00:33:29
一个请求发送出去之后,我们可以对HTTP的状态码做出断言,在TP单元测试扩展中对状态码做出响应主要有两个方法:
1. assertResponseOk:无参数,单元状态码为200
2. assertResponseStatus:接受一个状态码作为参数,并对这个状态码直接断言
# assertResponseOk
在快速入门的编写一个简单的测试用例中,我们只有两个测试类,一个是我们编写的,一个是安装扩展的时候自带的。只有两个测试类并且各自只有一个测试方法,理论上只有两个断言,但是却有两个测试类,四个断言。这是因为在单元测试扩展中,当我们构建一个请求的时候,扩展会使用本方法进行断言。
# assertResponseStatus
接受一个参数,该参数是要断言的状态码。
assertResponseOk内部便是使用该方法进行断言的。
# 例子
注意如果要使用redirect的话,就需要让控制器继承think\Controller。
~~~
redirect('http://www.baidu.com',302);
}
}
~~~
针对上面的控制器进行测试:
~~~
visit('/index/index/index')->assertResponseOk();
}
public function testNo(){
$this->visit('/index/index/index2')->assertResponseStatus(200);
}
}
~~~
>注意,这里的testNo应该是断言200的。在TP扩展中,assertResponseStatus目前阶段基本没有什么用处。因为当发生302的时候,扩展框架会重新就转移地址重新发起一个请求。因此上文中的转移到百度的页面,实际上还是会被重新转发到TP的App::run中,因此返回的结果会是test。例子如下:
~~~
public function testNo(){
$response=$this->visit('/index/index/index2');
$response->assertResponseStatus(200);//不是302
$response->see('test');//OK
}
~~~
# 对异常断言
当程序使用AOP来实现权限认证的时候,有可能当用户没有登录的时候,会返回401错误。但是在TP测试框架中,非200的状态码都会抛出一个异常,因此我们可以通过断言异常来判断权限认证:
```
/**
* @expectedException think\testing\HttpException
*/
public function testDeleteUser_noLogin(){
$this->visit('/admin/test/test');//由于用户没有登录,会返回401,而TP测试框架会抛出异常
}
```
';
对session进行断言
最后更新于:2022-04-02 00:33:27
# 说明
因为HTTP协议是无状态的,因此在网站开发的过程中我们经常使用session保存用户信息。
在ThinkPHP的单元测试扩展中,我们是可以对session做出断言的。
在该测试扩展中,对session断言的涉及到两个方法:
~~~
1. assertSessionHas($key, $value = null)
2. assertSessionHasAll(array $bindings)
~~~
第一个方法是断言session中是否有保存某个key,其值对应value。当只提供一个参数的时候,只判断session是否存在key,而不判断其值。
第二个方法是对key值数组进行判断。
# 例子
下面给出一个例子,用于测试session的两个方法:
~~~
visit('/index/index/addOneValue');
$response->assertSessionHas('value1');
$response->assertSessionHas('value1',1);
}
public function testAddTwoValue(){
$sessData=array(
'value2'=>2,
'value3'=>3
);
$response=$this->visit('/index/index/addTwoValue');
$response->assertSessionHas($sessData);
}
}
~~~
';
构建请求
最后更新于:2022-04-02 00:33:25
# 说明
在TP单元测试扩展中,模拟发送请求可以有5种方法,其中四种是扩展中自带的,另外一种是由我们自己通过request构建一个请求。扩展中带的四中分别是:
1. visit:发起一个GET的请求,只需要传入URI即可
2. submitForm:
3. makeRequest:
4. makeRequestUsingForm:
# visit
visit方法只接受一个参数uri,在这个参数传入我们的请求路径,扩展便会帮助我们构建一个GET的请求。其在底层是使用方法makeRequest进行构建的。
例子:
`$this->visit('/index/index/index');`
# makeRequest
$method:传输的方法
$uri
$parameters = []:所带参数
$cookies = []
$files = []
`$this->makeRequest('GET','/index/index/getMethod/name/c7')`
> 注意:请不要使用$parameters进行传参和使用?key=value方式传参,在目前的5.x中,这两种方式都会出现问题。这是因为在CLI下,创建请求的时候$_GET不会被设置,而普通的URL方式在5.X版本又不支持,因此才会出现这个问题的。解决方法是在request::create中添加一行代码:$_GET=$params;但是这个会修改到源代码,因此还是希望大家暂停使用这两种方法,等待官方的解决方案。
> 当使用$parameters进行传参的时候,如果是使用官方封装的request对象来获得参数,程序能够正常运行。但是切记不可使用$_GET等等超全局变量。
# submitForm
submitForm是基于makeRequest的,也就是说当使用makeRequest返回的内容中存在Form的话,我们就可以通过submitForm处理这个Form。
因此,我们需要构建两个操作,一个用于返回一个含有form的页面,一个用于接受这个form的请求。
index控制器如下:
~~~