验收测试 - 入门

介绍

单元测试主要针对函数/方法做测试,而验收测试则是主要针对页面做测试

  • 验收测试在Codeception里的名词是Acceptance

  • 测试用例的存放目录是/tests/acceptance

  • 配置文件是/tests/acceptance.suite.yml,但配置内容跟单元测试并不完全相同

  • 运行验收测试用例之前会运行/tests/acceptance/_bootstrap.php

  • 像单元测试一样,默认添加了一个空模块,你猜到了,就是/tests/_support/AcceptanceHelper.php!

  • 创建验收测试的命令是php E:\codecept.phar generate:cept acceptance IndexPage大概这样,下面会介绍


来来来聪明的小孩,咱们开始配置和编辑

开一下cmd谢谢

E:
cd project1-tests
php E:\codecept.phar generate:cept acceptance IndexPage

好了,然后提示E:\project1-tests\tests\acceptance\IndexPageCept.php已经创建好了,咱们去打开它准备编辑一下

不过另外要知道的是,接下来我们要针对页面进行测试,所以要先确定针对哪个页面做测试呢?这样吧,针对我这个博客网站的页面进行测试吧

先打开acceptance.suite.yml这个配置文件,修改modules - config - PhpBrowser - url这个配置的值,它默认值应该是http://localhost/myapp/的,我们修改成http://www.kkh86.com,这样就意味着接下来的测试主要是针对这个网站了,当然要跨网站的时候也是有办法的,再说吧

开始编写IndexPageCept.php,刚才的命令生成这个测试用例之后,它里面应该是自带了大概这样的代码的:

use project1_tests\AcceptanceTester;
$I = new AcceptanceTester($scenario);
$I->wantTo('perform actions and see result');

我们可以不修改这些,在下面第4行继续增加代码:

$I->amOnPage('/');	//切换到配置站点 http://www.kkh86.com 的 / 页面,拼起来就是 http://www.kkh86.com/
$I->see('我叫KK');	//断言可以在这个页面里看到指定的文字

运行和结果

这是很简单的测试代码,使用php E:\codecept.phar run acceptance IndexPageCept来运行它,输出结果应该是

OK (1 test, 1 assertion)

就是运行了1个测试用例,1次断言,测试通过,那你将$I->see('我叫KK')改成$I->see('我叫XX')再运行看看结果,失败了吧!?

我就不把运行结果贴出来了,因为经过之前的单元测试知识学习,你已经会简单地查看测试结果了,不然的话就是在低估你的智商了对吧哈哈-_-

验收测试 - 测试报告

紧接着上一章节测试失败的报告出来之后哦,你要注意,由于测试代码声明了要查看的特定字符串我是XX,但是实际上在页面上根本找不到,在失败报告出来后,为了让你自己进一步确认,测试框架会保存一份测试失败的时候的页面HTML代码给你复核,确认是不是真的这样,那这份HTML代码放在哪里呢?

且看tests/_output,这个目录本来是空目录来的,由于这次运行失败,测试用例的名称叫IndexPage,于是它就生成了一个叫IndexPageCept.fail.html的文件,打开里面就是一些HTML代码,你肯定很熟悉,没错,就是你要测试的页面的HTML代码,然后你可以手动搜索一下是不是真的没有我是XX这几个字————没吧?


结构化的报告

(新手可以先跳过,未来需要高级应用时再看这个)

运行的时候加入--xml参数的话就是要求生成xml格式的结构化测试报告,比如

php E:\codecept.phar run acceptance IndexPageCept --xml

这时候无论测试结果成功与否,都会在_output目录下产生一个叫report.xml的测试结果报告文件,具体这个文件有什么意义呢,在其它地方教大家部署自动化构建环境(持续集成)时再说吧,现在你只要知道有这东西就好

还有--html--json两个报告格式生成选项,你换着试一下就知道了,report.html,report.json,你懂的

验收测试 - 试着写几个行为

回顾之前入门的几个代码,我们大概翻译一下它(如果我翻译不准也不许投鸡蛋!)

use project1_tests\AcceptanceTester;		//引用这个“验收测试者“类
$I = new AcceptanceTester($scenario);	//实例化一个测试者,将全局变量$scenario传进去作为构造参数
$I->wantTo('perform actions and see result');	//我想执行一些动作并且看看结果
$I->amOnPage('/');	//我在 / 这个页面
$I->see('我叫KK');	//我看到“我叫KK"这串文字

发现没有,那些代码好像在讲英语一样耶,我想你可能是第一次接触这样有趣代码对吗!?至少我就是第一次在这个框架里接触这样的代码!太有创意了哈哈验收测试 4_html

Codeception在验收测试方面,定义了一个叫测试者的概念,就是上面new的那个类,一般测试者的实例变量名都叫$I,然后要测试的事情抽象出一个动词出来作为方法名称,比如see方法,就形成了$I->see($字符串)这样的代码,看着好像读英语一样。在英文文档中这个测试者应该叫Actor(演员/行动者?)


开始编码

好了概念说到这里,将以下代码贴进之前的测试用例里,覆盖掉原有的代码吧亲!

use project1_tests\AcceptanceTester;		//引用这个“验收测试者“类
$I = new AcceptanceTester($scenario);	//实例化一个测试者,将全局变量$scenario传进去作为构造参数
$I->wantTo('perform actions and see result');	//我想执行一些动作并且看看结果
$I->amOnPage('/');	//我在 / 这个页面
$I->see('我叫KK');	//我看得到“我叫KK"这串文字
$I->click('文章');	//我击带有“文章“这两个字的链接
$I->seeCurrentUrlEquals('/article/list.html');	//我看到当前网址是'/article/list.html'
$I->dontSee('我叫KK');	//我不想看到“我叫KK"这串文字
$I->seeElement('.pArcList');	//我看到class="pArcList"的一个元素
$menuText = $I->grabTextFrom('nav li:nth-child(3) a');	//我通过 nav li:nth-child(3) a 这个CSS选择器定位到一个元素并捕捉它里面的文本
\Codeception\Module\Asserts::assertEquals('心情', $menuText);	//调用断言模块断言变量
$I->click('nav li:nth-child(55) a');
$I->dontSeeCurrentUrlEquals('/article/list.html');	//我不想看到当前的网址是'/article/list.html'
$I->seeInTitle('音乐厅');	//我能在title里看到'音乐厅'三个字

$I->amOnPage('/about.html');	//我在'/about.html'这个页面
$I->seeNumberOfElements('.wrapTabContent', 3);	//我看到3个class="wrapTabContent"的元素

$I->amOnPage('/xxxxx.html');	//我在 '/xxxxx.html' 这个页面
$I->seePageNotFound();	//我看到页面不存在的错误(根据返回状态码是否404判断)

好了就写这么多代码先吧,你大概都能看懂,然后运行一趟,它提示

Trying to perform actions and see result (IndexPageCept)...

等了一会,报告OK (1 test, 9 assertions),结果全部测试通过了


看看失败的后果

然后你将中间那个$I->seeElement('.pArcList');修改成$I->seeElement('.xxxxx');,当然你也知道会运行失败,只是咱们看看它测试失败的报告是啥样子。..运行后报告内容大概是这样的:

1) Failed to perform actions and see result in IndexPageCept (E:\project1-tests\tests\acceptance\IndexPageCept.php)
Couldn't see element ".xxxxx":
Element located either by name, CSS or XPath '.xxxxx' was not found on page.

Scenario Steps:
6. I see element ".xxxxx"
5. I don't see "我叫KK"
4. I see current url equals "/article/list.html"
3. I click "文章“
2. I see "我叫KK"
1. I am on page "/"

可以说整份失败报告的信息都很有用

第1行告诉你哪个测试用例失败了(因为有时会同时运行多个测试用例,你需要知道在哪个失败了)

第2行告诉你失败原因

第3行进一步提示原因的细节

接下来就是Scenario Steps:栏目,它下面有6个运行步骤倒序排列,表示它从第1个测试动作运行到第6个动作,然后在第6个动作失败了


调试和查看测试进度

关于调试,其实跟单元测试里的调试一模一样,都要靠codecept_debug函数来输出调试数据,并且在运行时增加--debug参数

查看测试进度这个嘛,就是因为做这个验收测试一般会比单元测试慢很多,毕竟它要通过HTTP请求获取目标页面的内容,再从内容中搜索各种要see的东西,于是一个完善的验收测试跑下来你都要等老久,有时候你想知道它现在到底运行到哪了,则可以加上--steps参数,实例:

#可以多个运行参数一起上的!
php E:\codecept.phar run acceptance IndexPageCept --debug --steps --json

这样运行的时候它会每跑一个动作都在控制台有一个输出,告诉你它现在就在测试这个东西

  验收测试 - 元素定位

前面的代码你看到了,像$I->click('nav li:nth-child(55) a');这里是明显是通过CSS选择器定位到了一个a标签再执行了click

验收测试开发中最经常用到的就是元素定位了,不然的话怎么找到那个元素去确认它的各种情况呢对吧

那么定位方面Codeception就是提供了CSS选择器XPath的支持,但请你不要搞错成jQuery选择器了喔!我以前有过几位同事不小心写了jQuery的选择器类似nav li:eq(55) a这样,其实CSS选择器根本没有eq这个伪类嘛,所以他就定位失败,搞半天来问我,我一看就懂了...哈哈,所以写惯了jQuery的同学们必须注意这个问题!而且这里的CSS选择器是CSS3选择器,以后你定位不到元素上网搜索CSS3选择器就是了

后面一些断言方法的参数中会有一些叫做$selector的参数,指的就是CSS选择器XPath


......

.......

........

.........

..........

呃,好像这节内容就这么点?专门开一页?不如这样吧,有空你可以看看这下面,不然就跳过.

接下来做一些元素定位的练习吧^-^ XPath我不熟悉,下面演示的基本都用CSS选择器,我相信大家也是比较熟悉CSS的

(下面的定位器并不是针对我的网站写的,你们自己练习时将配置改成自己的网站,然后自己捏造元素吧!)

$I->see('登陆');		//无选择器,将直接在整个页面查找文本
$I->see('登陆', '#mainDiv form');		//在选择器所指的区域里开始查找文本
$I->see('登陆', '#mainDiv form button');		//更加精确了

$I->dontSee('登陆', '#mainDiv form');		//指定区域里排除文本,就是断言这个区域里不会有这个文本咯

$I->click('忘记密码');	//点击带有这"登陆"两个字的a标签
$I->click('忘记密码', '#mainDiv .loginForm');	//在指定区域开始查找这个文字的链接
$I->click('//form/*[@type=submit]');	//通过XPath定位到一个submit按钮并点击它,实现表单提交

验收测试 - 断言大全

好了其实各种see,dontSee,click,grabTextFrom这些方法都是断言方法,当它们期望的东西执行不成功时就会停止运行,跟单元测试里的assert系列断言方法的效果是一样的

其实有经验的人士应该想像得出来,就是通过curl把目标网站的源代码下载来,再往里面搜索字符串这样实现断言的

那么我们要针对页面测试时都能执行哪些断言呢?这实在是太多啦,抱歉我还没有精力把全部翻译并列出来,其实你可以查看tests/acceptance/AcceptanceTester.php这个文件的类里的方法,默认情况下它的所有方法就是可以使用的断言方法,比如开头你应该能看到一个叫setHeader的方法,则在编程时可以使用

$I->setHeader('content-type', 'application/json')

而不用像单元测试那样要调用$this->tester

接下来我主要介绍几个可能经常用得到的方法,大家可以参考这几个方法去理解其它类似的方法,毕竟不是全部人都能看懂官方那个类里面自带的英文注解的~

  • amOnPage($url) 将当前页面切换到指定的URL,这个url可以是完整的URL也可以是一个相对URL

  • amOnUrl($url) 切换当前基础网址,本身我们 $I->amOnPage('/index.html') 的话会默认相对于yml配置里的URL的嘛,现在如果通过这个方法切换成别的URL,则后面所有 amOnPage 方法的相对URL都是相对于这个新的URL,可以说是动态修改那个URL配置了,仅限该测试用例的当前会话生效,运行结束后下一个测试用例无效

  • see($text, $selector = null) 断言页面上会存在$text这个参数的字符,如果指定$selector的话则会在$selector里面查找这个字符

  • canSee($text, $selector = null) 跟see方法一样是查找文本的,但是如果找不到文本却不会停止运行,还有其它很多can开头的方法名称,都是断言失败不停止的方法,但失败会产生在报告里

  • click($link, $context = null) 点击一个链接,这样会导致当前页面变更哦

    $link参数可以是a标签里的文字,也可以是选择器

    但是其实如果button的name或value的值符合$link的话都会被定位到哦

    对于img标签则会把alt属性也加入匹配定位的内容中

    最后呢如果匹配到的是一个type=submit的button的话则会同时触发表单的提交

  • fillField($field, $value) 向name="$field"这个表单项填充$value这个值

    比如你有一个注册页,用select控件来选择性别,value=2就是女性

    $I->fillField('sex', 2);		//选择女性
    
  • seeInField($field, $value) 断言name="$field"的表单项的value值与$value是匹配的

    接上面fillField的例子,默认性别是未知,value是0,做一个表单修改的测试

    $I->seeInField('sex', 3);	//断言默认是3
    $I->fillField('sex', 2);		//填充2
    $I->seeInField('sex', 2);	//断言填充后就是2,但实际是填充后再断言,在基础的验收测试里意义不大,用在后面的WebDriver验收测试中才最能彰显测试效果
    
    $I->amOnPage('/user-center.html');	//假如切换到注册页面
    $I->seeInField('username', '请填写用户名');	//断言用户名的默认值
    
  • seeCheckboxIsChecked($checkbox) 断言$checkbox所指的勾选项是已经勾选了的($checkbox也是一个选择器!个人觉得该参数应该命名为$selector)

    然后如果要断言是未勾选的就是用dontSeeCheckboxIsChecked($checkbox)

  • submitForm($selector, $params, $button = null) 将$selector选中的表单发起提交,$params是key => value表达的表单参数值,这样你就不需要慢慢用 $I->fillField 这些方法来填充表单而是在这里直接传递参数了, $button 是提交按钮的选择器,可以不填,但如果存在 修改/删除 等多个提交按钮时就需要用 $button 了

  • seeCookie($name) 断言存在指定$name的Cookie

  • resetCookie($name) 删除cookie

    断言没有指定Cookie的话当然也是用dontSeeCookie($name)了,其实好多see方法都有一个对应的dontSee方法和canSee方法

  • canSeePageNotFound 断言当前是404页面,之前的例子里你有见过,我这里不举例了

  • grabValueFrom($field) 获取HTML中name="$field"这个表单项的值,但这里要注意,这个表单项必须要被form标签包起来哦,我试过有一次对一个select标签的值死活取不出来,后来发现form里的能拿出来,于是才注意到有这个...

    而关于这个方法的使用嘛,我要举一下例子,为什么呢?因为验收测试中默认是没有assert系列的断言方法的!你想想喔,如果你想测试的页面是一个注册页,用select控件来选择性别,默认是未知,value是3,然后你想测试时确认这个值默认是3怎么办?我认为只能这样:先通过grabValueFrom方法将表单值获取出来,再用断言方法断言这个值,代码如下:

    //顶上要 use \Codeception\Module\Asserts;
    
    $gender = $I->grabValueFrom('sex');
    Asserts::assertEquals(3, $gender);
    

    这样来断言,其实之前的例子有刻意添加过这个演示代码,具体嘛,将会在 验收测试 - 扩展 章节中解释,反正如果你要断言的话就要引用Asserts,然后再通过静态方法来调用断言方法,它的断言方法和PHPUnit差不多

  • amOnSubdomain($subdomain) 切换到子域名,比如配置时URL是 qq.com,或者 www.qq.com, 又或者是 shop.qq.com ,执行

    $I->amOnSubdomain('pay');
    

    则是意味着

    $I->amOnPage('/xxx.html');
    

    会切换到 http://pay.qq.com/xxx.html

验收测试 - 断言变量

验收测试针对页面进行测试,一般情况下更多的是$I->see('文本')断言页面上有特定的文本,或者$I->seeElement('#xxx')这样有特定的dom元素,但有时候我们要断言一些变量的值,你想使用$I->assertEqual(3, $var)默认情况下那可不行,因为验收测试是使用PhpBrowser模块实现的,而这个模块并没有assert系列的方法,所以AcceptanceTester测试器自然就没有assert方法,$I变量也就调用不了这些方法咯

要让测试器拥有类似单元测试的断言方法,需要修改acceptance.yml添加Asserts模块,但这里添加模块的时候要注意一下配置文件

回顾一下单元测试的模块配置,是这样的吧:

enabled: [Asserts, UnitHelper]

这样表示单元测试引入了两个模块,但是验收测试就不同了,它的enabled配置名称后面没有用方括号包住两个模块,而是分成两行,这样:

enabled:
    - WebDriver
    - AcceptanceHelper

这样和方括号是一样效果的,大家完全可以修改成

enabled: [WebDriver, AcceptanceHelper]

所以如果不修改的话,要添加Asserts模块则是在下面增加一行- Asserts即可

接下来配置好之后请务必记得重构测试器,然后就可以执行$I->assertEquals(3, 3)这样了


然并卵

虽说引入这个Asserts模块后使得验收测试既能断言dom又能断言变量,但是实际上你会发现$I->assertInternalType('string', $str)的时候就会报错说你调用了一个不存在的方法,这难道不是已经引入了Asserts模块了吗?

是的,你是引入了,但是很抱歉地告诉你,这个Asserts其实只有二十个断言方法左右.它并不拥有PHPUnit框架的所有断言方法.

而单元测试里之所以能用那么多断言方法是因为它是继承了PHPunit的断言类,但这个Asserts模块并不继承于它,有兴趣的朋友可以看看Codeception的源代码.

那又不提供所有PHPUnit的方法,要用到的时候没得用,这算啥呢?

我只能希望框架开发团队未来能完善这个断言模块吧,其实这个Assert模块直接继承PHPUnit的类多好,这样就全部拥有了~浅见而已,他们不这样做是不是有别的原因咧?


来点实际的

上面我只是告诉大家一些真相,然而大家无论知不知道,反正不用那个Assert模块就不会有啥问题,但是在验收测试的时候,我们还是有必要去断言变量的,既然Asserts模块不能给到我们全部,我们就找PHPUnit就行了

Codeception已经将PHPUnit集成在里面了,因此我们随时可以调用PHPUnit的断言功能.为了方便,我们在使用之前先在顶上声明use PHPUnit_Framework_Assert;

然后就可以

PHPUnit_Framework_Assert::assertTrue(1 == 2);
PHPUnit_Framework_Assert::assertEquals(2, 2);
PHPUnit_Framework_Assert::assertInternalType('string', 'abc');

而后面的验收测试代码中我们偶尔还真的需要使用它去帮我们断言一些变量

验收测试 - 不能执行JS

当要测试的页面上存在JS标签或者JS代码时,它们不会被运行起来,比如有这样的JS代码:

window.onload = function(){
	var div = document.getElementById('status');
	div.innerText = '数据获取中...';
};

假如#status的text本来是空的,这段JS就是在页面加载完毕之后将这个#status的内容设置成一个提示文本

我们用各种安防软件设置本机网速限制为几KB一秒,再用浏览器人工访问这个页面可以看到,由于网页慢,页面代码未加载完毕,#status是空的,加载完成后就由于JS代码被运行所以出现了提示语句

但是在Codeception的基础页面测试中并不是这样,以下断言是不会成功的

$I->see('数据获取中...', '#status');

原因就是 #status 始终都是空的,因为JS没有被运行起来,这是因为基础的验收测试是靠CURL获取页面源代码,再从源代码中构造DOM树,从DOM树中实现了测试代码的断言行为

但并没有提取源代码中的JS部分运将JS代码运行起来,这也需要一个JS引擎才可以实现,但可以告诉你这个codecept.phar包中并不包含JS引擎,呵呵,就不多啰嗦了,我想你都是做了1年左右的PHP了,我说这么点你已经知道具体原因了

但是要实现运行JS也并非不可能,后面的 WebDriver验收测试 是可以帮到你的,且淡定地先把基础的验收测试学好吧

验收测试 - 测试目标

一句话总结:最好就是测试代码放在本地服务器,测试目标也是公司本地服务器的测试网站,尽量不要针对线上服务器做增删改测试

详解

像这样的测试注册功能的代码

$I->amOnPage('/register.html');
$I->fillField('email', 'xxx@yy.com');
$I->fillField('password', '121212');
$I->submitForm('#registerForm');

写好后,为了确保注册功能是正常的,理想状态下应该让它每天都运行至少1次

但是要测试的网站是哪个呢?你有本地开发的测试网站,也有外网正式部署的网站呀

这里我没有绝对的答案,问过前辈们也得不到最终答案。

其实只要确认好本地测试站没事,线上站一般都不会有事的,除非你的代码够乱

所以只要确保本地测试网站正常就基本可以了(中大型项目另谈)


我想针对线上进行测试好吗?

如果这个注册代码针对线上做测试,你需要解决一个问题就是“删除测试数据"

每天都测试注册,肯定会产生一个注册用户的数据嘛,然后你要删除他,可能有N个关联表的初始化信息都要删除是不是

如果注册时还有地区这种表单,填了北京作为测试地区,然后确认注册,刚好这一时刻有个用户查找可能认识的人或查找北京的人,对你这个用户加好友,然后好友申请表中有关于这个用户的ID了,又或者可能认识的人的ID集中冗余了这个ID,呵呵,这时候你除了要删除基础的用户数据表,还要找到可能关联的位置把ID都灭掉

所以删除数据绝对麻烦死你,虽然并非做不到那么狠


我还是想针对线上进行测试好吗?

"我总是觉得,测试线上才是最实际的!有时候偏偏就是线下没问题,线上出问题呀!"

我不否认出问题的情况会有差异,但只能断定为你对项目的规划和程序设计方面并不到位才造成了问题,稳定的程序基本不会出现这个问题的

但我依然不赞成你对线上做测试!

注意你测试的其实不只是注册功能吧,登陆好说,填东西点击登陆成功就是了,但你还可能有评论测试,赞的测试,领取礼物的测试啥的

我问你,评论测试的话,你测试的评论被发表出去,要是被别人引用回复了怎么办,你销毁测试评论的时候是不是要把别人的回复也灭了才行,不然别人引用的回复找不到数据可麻烦了。...而且这是程序自动化测试,不是人工测试,所以不能智能地写评论,只能把你填好的字符串提交上去,或者随机字符,如果你说不删除测试数据,是不是就纵容这些测试评论一天一天地堆积?一天测试一次量肯定不大,其实你还不知道有的项目是一小时测试一次的,狠不狠?人家就是那么重视质量

我问你,删除数据怎么删除,你的测试代码一般是不能跟产品服务器放在一起的,就像之前针对我的博客网站测试一样,你的代码不必放在我网站服务器上嘛,你也可以运行起来对我网站各页面做各种断言。于是,如果我的网站是属于你自己的,你测试完注册用户后,要删除一个注册用户,怎么实现删除?

  • 远程连接网站数据库做数据删除吗----实际上大部分网站的数据库是不对外开放连接的,所以你顶多是对自己的小站点实施这个方案,我服了你

  • 在网站下添加一个叫delete-test-data.php的文件,测试完后再来一句$I->amOnPage('/delete-test-data.php')来触发删除脚本----后果就是如果这个入口被外人知道了的话他也能通过浏览器输入来触发删除,一旦你代码写不好可能就被这个触发误删了些东西

  • 将本地测试服务器的ssh公钥放到线上服务器中,然后发送通知到线上服务器进行数据删除----这个还算可以,但实际上允许了通过ssh做更多事情。..公司内其他同事一旦控制了这台测试服务器,你不能保准他们会干嘛

更多更多。..

最终,删除测试数据真是麻烦的事,而且通常是很可能删不干净的,导致数据库有坏数据,不必要的冗余等。

听我的,别对线上服务器做增加数据的操作,包括修改的,因为修改后,你又要将数据恢复是不是?比如修改年龄啊,本来是18岁,你改成20岁,如果你不恢复,明天测试时又修改成20岁,网站应该会提示“您的信息没有变更“啥的吧

删除数据的测试更加不行啦哈哈,比如你测试删除一个好友后,又要实现恢复好友数据的代码,或者删除评论,又要恢复评论,而且你删除后没恢复期间有别的数据间插入来扰乱了原来的顺序就可能会导致麻烦。

所以呢,听我的,增删改的操作最好别对线上服务器做测试,因为数据变更好,要恢复到原来的状态是非常非常麻烦的事情。毕竟数据经常有多方面的关联性

只做查询的测试倒是可以,起码能确认页面是正常的,或者个别数据的增删改测试后,不恢复的话都完全不会造成影响就是,这毕竟也只是少数

一般本地测试网站怎么增删改数据都无所谓,不搞恢复工作也无所谓,因为可以直接从线上镜像一份数据下来,或者先备份后测试,再恢复备份数据库

验收测试 - 验证码的问题

重复一下之前这段针对注册表单做测试的代码,我下面加一些内容请你注意:

$I->amOnPage('/register.html');
$I->fillField('email', 'xxx@yy.com');	//填充邮箱
$I->fillField('password', '121212');		//填充密码
$I->fillField('verify', '填什么好呢');		//填充验证码
$I->submitForm('#registerForm');

其实一般都会有个验证码,那就是验证码方面我们填什么好呢?你的程序不容易自动识别验证码图片的文字呀,如果你写死一个验证码或者随机生成一个填写,那99.999999%会提示你验证码错误咯,导致注册能否正常都难以测试.

所以本地测试又有这样一个好处,我们可以在网站程序里大概这样根据环境判断结果来生成不同的验证码:

if(PROJECT_ENV == 'test'){
	$verifyImage->drawText('121212');	//固定测试服务器的所有验证码为121212
}else{
	$verifyImage->drawText(mt_rand(138772, 923839));	//随机生成
}

这样测试代码就可以总是填写121212这个验证码了

另外其实这样不止是对自动化测试有好处,对人工测试也有好处,你就不必耗费脑力去分析验证码是什么文字啦,很快速地填写121212这个验证码就行了,关于为什么使用121212方面你可以转到我这篇经验分享中了解一下 经验分享 - 测试 - 统一密码和验证码

验收测试 - 最好别写死URL

比如切换到一个5号分类商品页面$I->amOnPage('/product/5')

但我并不建议大家实际编程时真的这样写死URL,因为以后如果你们的URL规则变了呢,比如变成了/shop/5.html这样子,测试用例又要修改相关的URL啦,好麻烦的

多数框架都提供了生成URL的方法,我们在_bootstrap.php里引入项目的框架,做好初始化工作,然后就大概这样实际应用(我拿Yii2框架打比方):

//上面 use yii\helpers\Url;

$I->amOnPage(Url::to(['product/category', 'id' => 5]));

这样的话,只要控制器和方法名称不变就好,实际生成后的伪静态URL就看项目配置了嘛

验收测试 - Ajax

之前跟大家演示的都只是些页面URL、元素和文本的断言,但是如果要异步获取一个数据列表的数据时,我们通常就要往一个地址获取json数据回来再用js把数据渲染到列表里,好了我们如何测试这个异步获取数据列表的接口输出的数据是否正常呢?

由于它输出的数据是json,假设它的地址是datalist.php,那么我们也不能这样实现断言

$I->amOnPage('/datalist.php');
$I->see('你一定很头疼这里写什么');
$I->seeElement('就一串JSON字符串,哪有什么Html Element?');

来,正题,问题就在上面,解决办法在此

$param = [
	'page' => 2,	//假设我们要第2页的数据
	'type' => 3,	//假设数据有类型~
];	//这个$param是要异步请求时提交上去的参数
$I->sendAjaxRequest('get', '/datalist.php', $param);	//这样其实就相当于 /datalist.php?page=2&type=3
$I->seeResponseCodeIs(200);	//断言请求后,服务端响应回来的报文状态码应该是200

$browser = \Codeception\SuiteManager::$modules['PhpBrowser']; //这样来取出一个模块
$jsonString = $browser->client->getInternalResponse()->getContent()->__toString(); //通过模块获取响应正文,就是那串json,但必须转成string(注意我代码后面有toString的调用),否则你会得到一个对象,这框架抽象得挺厉害,连个响应报文内容都是对象

$jsonArray = json_decode($jsonString, true);
\PHPUnit_Framework_Assert::assertInternalType('array', $jsonArray);	//断言解码后的类型
\PHPUnit_Framework_Assert::assertEquals(20, count($jsonArray));	//断言数据个数

这是一个断言ajax请求的例子,其实这里用到的几个方法也没写到前面的断言大全里,因为我想你慢慢深入,不要一下子迎来太多东西.但其实经验丰富的人士就算不看英文文档,光看AcceptanceTester这个类的方法就已经知道有这些东西了

好了我下一节继续补充一下断言

验收测试 - 断言大全 - 补充
  • sendAjaxRequest($method, $uri, $params = null):发送一个ajax请求,$method是请求的方式,比如get,post,delete,put,也可以自定义请求方式,具体看服务端程序是否扩展了这个请求方式并作响应了

  • sendAjaxGetRequest($uri, $params = null)用get方法发送一个ajax请求,跟sendAjaxRequest('get', ...)是一样的

  • sendAjaxPostRequest($uri, $params = null)用get方法发送一个ajax请求,跟sendAjaxRequest('post', ...)是一样的

  • seeResponseCodeIs($code) 断言上次发生HTTP请求后的响应状态码

  • setHeader($header, $value) 设置下一次请求的header

  • attachFile($field, $filename) 附加一个文件,以$field这个字符串变量来命名,比如$field叫image的话,就是PHP角度访问的那个$_FILES['image']了; 而$filename就是附加的文件是哪个文件,是你当前测试用例所在磁盘上的物理路径哦

  • amHttpAuthenticated($username, $password) 用指定的用户名和密码提交到当前的Http认证中,认证不通过将导致断言失败

就写到这了,我还真写不完整个AcceptanceTester的断言,什么时候闲得蛋疼了,或哪位朋友有空义务帮忙写写再说吧~~帮忙写的话用markdown哦^-^

验收测试 - Db模块协助

好了,希望你也认同验收测试代码应该是部署在本地服务器,然后最好也只针对本地测试网站做测试这个理念,这样才好学本节的内容。

其实不管怎样,本节内容的前提就是:测试代码和被测试的网站都处于同一个内网,或者有办法共同连接同一个数据库

这样就好玩多了,因为这里还能够去测试数据库的变更和站点UI是否相对,中间缓存又是否已经生效。

添加模块

首先我们要为验收测试添加一个叫Db的模块,它是自带模块,只要在配置文件上追加一下名称并重构测试器即可

但是追加模块名称后还需要配置数据库的用户和密码信息以便这个模块能连接到数据库上,在codeception.yml全局配置的modules - config - Db里为Db模块配置各项信息,它默认提供了几个配置选项(即使以前没有使用该模块,但有这个信息配置存在也不会影响),而dsn就是PDO的dsn了,因为它是靠PDO来靠作数据库的


实践

然后比如要测试"赞"的功能正确

$testBookId = 99;	//要测试的书籍ID
$bookData = $I->grabFromDatabase('book', '*', ['id' => $testBookId]);	//从数据库取出书籍记录

$I->sendAjaxPostRequest('/book/support.do', ['id' => $testBookId]);	//假设这是对 ID为99的一本书进行"赞/喜欢"操作

$I->seeInDatabase('book', [
	'id' => $testBookId,
	'support' => $bookData['support'] + 1,
]);	//断言support字段被加1了

$I->dontSeeInDatabase('book', [
	'id' => $testBookId,
	'support' => $bookData['support'],
]); //或者断言不会是原来的值

由于测试目标网站和测试代码连的是同一个数据库,所以理论上从数据库读出的数据跟网站上呈现的数据应该是能对得上的,这样你就能确保一些读取数据库数据显示的代码是设计正确的了

个人经常用的是往数据库里随机抽一条数据记录,然后拿这个数据去站点的某个表单中填一填看看反应是不是正确的


还有个haveInDatabase方法,我觉得用途不是很大,有兴趣的话自己看看测试器的备注试用一下吧

验收测试 - 基础 - 自动恢复测试数据

测试过程中可能会产生一些数据,比如

  • 测试添加一个用户,这样数据库里就会多一个用户了,平均每天运行约5次添加用户的测试的话,则100天后数据库就会多出500个用户了。。。其它数据也是这样滚雪球一样累加

  • 测试抽奖,于是大奖可能被抽到了,下次再测试就没有了大奖可抽

  • 测试禁用功能,数据status被标记为禁用后,下次再测试禁用就会update影响行数为0了

  • 测试删除指定数据,下次再测试又找不到这条数据删除了


所以大家迟早都会慢慢地遇到这些需要重复测试的数据支持功能

我以前不会弄的时候真的好笨,添加的用户在after方法里执行删除,抽过的奖也重新标记为未抽奖。。。

这样就写了好多数据恢复代码,真是累觉不爱!想想感觉自己也有点强大,整个项目下来N多测试,就这些数据撤回的代码居然都全给写下了测试代码,把一个个测试场景的数据恢复了验收测试 4_数据库_02


发现有捷径

怪我以前没不够认真看Db模块的教程啊,原来它可以支持快速恢复数据!

还记得在phpmyadmin导出的.sql文件,以及mysqldump命令导出的文件吗?里面其实就是一句句SQL语句是吧,导入时,就是执行了这些语句,然后就往数据库里重新建立了整个数据库

其实Codeception的原理也是一样,实现步骤是这样的:

  1. 我们先导出一个.sql文件,然后放在tests/_data目录下命名为dump.sql

  2. 在测试项目的根目录的codeception.y2ml配置Db模块

    验收测试 4_验收测试_03

    里面除了配置dsn、username、password这些选项满足连接数据库以外,还有一个叫dump的配置选项,它要求提供一个测试项目的相对文件路径,这个文件就是我上面讲的数据库导出文件,一般后缀我们都命名为.sql

  3. 新建空的数据库,比如dsn里配置的数据库名称叫shop的话,那你就要在数据库服务器上先准备一个空的shop数据库

  4. 在需要测试的测试类型配置文件中开启Db模块,比如要在单元测试中使用自动恢复数据功能,就要在unit.suite.y2ml里开启Db模块了,记得build一下

然后每次运行测试的时(不论是单元测试还是验收测试),Codeception都会将这个dump.sql作为数据库镜像导入到dsn所指的数据库中,于是测试代码就可以基于全新的数据进行测试了

运行完测试后你可以发觉shop数据库已经不是一个空数据库,而是有表有数据的,正好就是dump.sql里的东西

下次再测试时,它又重新导入一次,就算shop已经有了数据,Codeception也会先清空这些数据,再将dump.sql导入


注意点:dump.sql里的建表语句必须有IF NOT EXISTS判断,就以phpmyadmin导出的数据库文件为准,这是直接可以用来做dump.sql的,如果没有的话会导致第二次运行时无法覆盖已有数据,具体细节感兴趣的话请看Codeception源码

验收测试 4_html_04

验收测试 - 扩展(模块)

和单元测试一样,test/acceptance.suite.yml里的modules - enabled里面是验收测试的模块配置,

于是你也知道了,验收测试用了PhpBrowserAcceptanceHelper这两个模块,而AcceptanceHelper就是_support目录下的AcceptanceHelper,默认是个空类


增加我们的方法

那当然就是像当然测试那样,对自带的tests/_support/AcceptanceHelper.php这里的类做编辑,添加你想要的方法咯,比如我添加这样一个方法:

public function login($username, $password){
	$browser = $this->getModule('PhpBrowser');	//要先取出浏览器模块,这个模块必须是yml里已经配置了的,否则get出个null
	$browser->amOnPage('/login.html');	//其实之前测试用例的$I调用的就是PhpBrowser模块的方法,这里直接用browser来调用效果是一样的
	$browser->see('登陆', '#loginForm');
	$browser->fillField('username', 'test1');
	$browser->fillField('password', '121212');
	$browser->fillField('verify', '121212');
	$browser->submitForm('#loginForm');
	$browser->see('会员中心', '#position');
}

然后重构一下测试器,这里它会同时将单元测试的Tester也一起重新构造,尽管你没对单元测试的模块做改变

然后可以看到tests/acceptance/AccepanceTester.php里多了一个login($username, $password)方法

那么你的测试用例可以这样写了:

use project1_tests\AcceptanceTester;
$I = new AcceptanceTester($scenario);
$I->login();
$I->amOnPage('/friends');	//假设去好友查看页面
$I->see('可能认识的人');	//假设去商店
$I->see('下一页');	//断言有下一页按钮
$I->seeNumberOfElements('#friends li', 20);	//断言好友列表里有20个
//....更多登陆后才能做的断言

于是,你也能封装更多更多的方法到Helper里了

 

学习时的痛苦是暂时的 未学到的痛苦是终生的