单元测试一个ZF2应用程序
来源:互联网 发布:ubuntu wine 使用 编辑:程序博客网 时间:2024/06/13 07:59
在一个大型项目的开发过程中特别是牵涉到许多人员参与时,一个可靠的单元测试是必不可少的。对于应用程序每当有所变化后都返回并手动对每个组件进行测试是不切实际的。在你用相同的方法写自己的测试时单元测试可以帮助减轻工作量,它可以自动测试你的应用程序中的组件并且在某些组件不工作时对你发出提醒。
这篇教程希望能够展示如何在ZF2 MVC应用程序中测试不同的部分。同样的,这篇教程将继续使用在快速教程中的应用程序。它不是一般单元测试指南,但是这里只是帮助克服首次在ZF2应用程序中编写单元测试的障碍。
建议读者基本了解单元测试,断言(assertions)和mocks测试。
本教程中,ZF2框架API使用PHPUnit,假设已经安装好了PHPUnit,PHPUnit版本应该是3.7.*
一、设定测试目录
由于ZF2的应用程序是由独立的应用程序模块所构建的,我们不整体的测试应用程序,而是一个模块一个模块的测试。
我们将展示如何构建测试模块的最低要求,我们在快速学习中编写的唱片模块以及任何其它可以作为基本测试的模块。
从在zf2-tutorial\module\Album目录下建立test目录开始,构建以下的目录结构
zf2-tutorial/
/module
/Album
/test
/AlbumTest
/Controller
test目录的结构完全匹配模块的源文件,它可以让你保持你的测试井井有条,很容易找到。
二、引导你的测试
接下来在zf2-tutorial/module/Album/test目录下创建一个phpunit.xml文件,代码如下:
<?
xml
version
=
"1.0"
encoding
=
"UTF-8"
?>
<
phpunit
bootstrap
=
"Bootstrap.php"
colors
=
"true"
>
<
testsuites
>
<
testsuite
name
=
"zf2tutorial"
>
<
directory
>./AlbumTest</
directory
>
</
testsuite
>
</
testsuites
>
</
phpunit
>
<?php
namespace
AlbumTest;
use
Zend\Loader\AutoloaderFactory;
use
Zend\Mvc\Service\ServiceManagerConfig;
use
Zend\ServiceManager\ServiceManager;
use
RuntimeException;
error_reporting
(E_ALL | E_STRICT);
chdir
(__DIR__);
/**
* Test bootstrap, for setting up autoloading
*/
class
Bootstrap
{
protected
static
$serviceManager
;
public
static
function
init()
{
$zf2ModulePaths
=
array
(dirname(dirname(__DIR__)));
if
((
$path
=
static
::findParentPath(
'vendor'
))) {
$zf2ModulePaths
[] =
$path
;
}
if
((
$path
=
static
::findParentPath(
'module'
)) !==
$zf2ModulePaths
[0]) {
$zf2ModulePaths
[] =
$path
;
}
static
::initAutoloader();
// use ModuleManager to load this module and it's dependencies
$config
=
array
(
'module_listener_options'
=>
array
(
'module_paths'
=>
$zf2ModulePaths
,
),
'modules'
=>
array
(
'Album'
)
);
$serviceManager
=
new
ServiceManager(
new
ServiceManagerConfig());
$serviceManager
->setService(
'ApplicationConfig'
,
$config
);
$serviceManager
->get(
'ModuleManager'
)->loadModules();
static
::
$serviceManager
=
$serviceManager
;
}
public
static
function
chroot
()
{
$rootPath
= dirname(
static
::findParentPath(
'module'
));
chdir
(
$rootPath
);
}
public
static
function
getServiceManager()
{
return
static
::
$serviceManager
;
}
protected
static
function
initAutoloader()
{
$vendorPath
=
static
::findParentPath(
'vendor'
);
$zf2Path
=
getenv
(
'ZF2_PATH'
);
if
(!
$zf2Path
) {
if
(defined(
'ZF2_PATH'
)) {
$zf2Path
= ZF2_PATH;
}
elseif
(
is_dir
(
$vendorPath
.
'/ZF2/library'
)) {
$zf2Path
=
$vendorPath
.
'/ZF2/library'
;
}
elseif
(
is_dir
(
$vendorPath
.
'/zendframework/zendframework/library'
)) {
$zf2Path
=
$vendorPath
.
'/zendframework/zendframework/library'
;
}
}
if
(!
$zf2Path
) {
throw
new
RuntimeException(
'Unable to load ZF2. Run `php composer.phar install` or'
.
' define a ZF2_PATH environment variable.'
);
}
if
(
file_exists
(
$vendorPath
.
'/autoload.php'
)) {
include
$vendorPath
.
'/autoload.php'
;
}
include
$zf2Path
.
'/Zend/Loader/AutoloaderFactory.php'
;
AutoloaderFactory::factory(
array
(
'Zend\Loader\StandardAutoloader'
=>
array
(
'autoregister_zf'
=> true,
'namespaces'
=>
array
(
__NAMESPACE__ => __DIR__ .
'/'
. __NAMESPACE__,
),
),
));
}
protected
static
function
findParentPath(
$path
)
{
$dir
= __DIR__;
$previousDir
=
'.'
;
while
(!
is_dir
(
$dir
.
'/'
.
$path
)) {
$dir
= dirname(
$dir
);
if
(
$previousDir
===
$dir
)
return
false;
$previousDir
=
$dir
;
}
return
$dir
.
'/'
.
$path
;
}
}
Bootstrap::init();
Bootstrap::
chroot
();
现在,如果你转到zf2-tutorial/module/Album/test/目录并且运行phpunit,你将得到类似以下的结果
PHPUnit 3.7.13 by Sebastian Bergmann.
Configuration
read
from
/var/www/zf2-tutorial/module/Album/test/phpunit
.xml
Time: 0 seconds, Memory: 1.75Mb
No tests executed!
即使没有执行测试,我么至少知道自动调用找到了ZF2的文件,否则它会抛出一个RuntimeException,这是定义在引导文件的第74行。
三、你第一个控制器测试
测试一个控制器永远不是一个简单的任务,但是ZF2框架的Zend\Test可以减少非常多的麻烦。
首先,在zf2-tutorial/module/Album/test/AlbumTest/Controller目录下建立一个IndexControllerTest.php文件,代码如下:
<?php
namespace
AlbumTest\Controller;
use
Zend\Test\PHPUnit\Controller\AbstractHttpControllerTestCase;
class
AlbumControllerTest
extends
AbstractHttpControllerTestCase
{
public
function
setUp()
{
$this
->setApplicationConfig(
include
'/var/www/zf2-tutorial/config/application.config.php'
);
parent::setUp();
}
}
我们在这里扩展的AbstractHttpControllerTestCase类帮助我们设置应用程序自身,有助于在发生请求过程中的调度和其它任务,同样提供声明请求参数的方法,响应头,从定向以及更多。更多内容见Zend\Test文档。
有一件需要说明的事情是使用setApplicationConfig方法设置应用程序的配置。
下载,在AlbumControllerTest类中添加以下函数
public
function
testIndexActionCanBeAccessed()
{
$this
->dispatch(
'/album'
);
$this
->assertResponseStatusCode(200);
$this
->assertModuleName(
'Album'
);
$this
->assertControllerName(
'Album\Controller\Album'
);
$this
->assertControllerClass(
'AlbumController'
);
$this
->assertMatchedRouteName(
'album'
);
}
注意:
为声明控制器名称,我们使用的是我们定义在唱片模块路由配置中的控制器名称。在我们的例子中这定义在唱片模块中module.config.php文件的19行。
四、一个失败的测试例子
最后,进入zf2-tutorial/module/Album/test/并且运行phpunit。呕!测试失败了
PHPUnit 3.7.13 by Sebastian Bergmann.
Configuration
read
from
/var/www/zf2-tutorial/module/Album/test/phpunit
.xml
F
Time: 0 seconds, Memory: 8.50Mb
There was 1 failure:
1) AlbumTest\Controller\AlbumControllerTest::testIndexActionCanBeAccessed
Failed asserting response code
"200"
, actual status code is
"500"
/var/www/zf2-tutorial/vendor/ZF2/library/Zend/Test/PHPUnit/Controller/AbstractControllerTestCase
.php:373
/var/www/zf2-tutorial/module/Album/test/AlbumTest/Controller/AlbumControllerTest
.php:22
FAILURES!
Tests: 1, Assertions: 0, Failures: 1.
protected
$traceError
= true;
Zend\ServiceManager\Exception\ServiceNotFoundException: Zend\ServiceManager\ServiceManager::get
was unable to fetch or create an instance
for
Zend\Db\Adapter\Adapter
对于这个错误信息,很清楚在服务管理器中没有提供给我们所有的依赖性。让我们来看看如何修复这个错误。
五、为测试配置服务管理
错误信息说明服务管理器不能为我们创建数据库适配器的实例。数据库适配器间接的被我们的Album\Model\AlbumTable所使用从数据库中获得唱片列表。
第一个想法是创建一个适配器实例,把它传递给服务管理器并且让代码在那里运行。这样做的问题是我们结束我们的测试案例实际上是对数据库进行了查询。为了保持我们测试速度快,在我们的测试中尽可能减少错误点的数量,这点应该被避免。
第二个想法是创建一个模拟的数据库适配器,并且阻止真实的数据库调用。这是好得多的方法,但是创建模拟适配器是乏味的(但是无疑我们在某个时刻必须创建它)
最好的方法是模拟出我们的Album\Model\AlbumTable类,它从数据库中检索出唱片列表。记住,我们现在正在测试我们的控制器(Controller),所以我们可以模拟真实的调用fetchAll以及使用虚拟的值来替换返回的值。在这点上,我们对fetchAll如何检索唱片不感兴趣,只对它被调用并且返回一个唱片列表有兴趣,这就是为什么我们可以模拟成功。当我们要测试AlbumTable自身,我们将为fetchAll方法写真实的测试。
这里是我们如何实现这个,按照以下代码修改testIndexActionCanBeAccessed测试方法
public
function
testIndexActionCanBeAccessed()
{
$albumTableMock
=
$this
->getMockBuilder(
'Album\Model\AlbumTable'
)
->disableOriginalConstructor()
->getMock();
$albumTableMock
->expects(
$this
->once())
->method(
'fetchAll'
)
->will(
$this
->returnValue(
array
()));
$serviceManager
=
$this
->getApplicationServiceLocator();
$serviceManager
->setAllowOverride(true);
$serviceManager
->setService(
'Album\Model\AlbumTable'
,
$albumTableMock
);
$this
->dispatch(
'/album'
);
$this
->assertResponseStatusCode(200);
$this
->assertModuleName(
'Album'
);
$this
->assertControllerName(
'Album\Controller\Album'
);
$this
->assertControllerClass(
'AlbumController'
);
$this
->assertMatchedRouteName(
'album'
);
}
运行phpunit命令,我们将获得以下的输出信息并且测试通过了
PHPUnit 3.7.13 by Sebastian Bergmann.
Configuration
read
from
/var/www/zf2-tutorial/module/Album/test/phpunit
.xml
.
Time: 0 seconds, Memory: 9.00Mb
OK (1
test
, 6 assertions)
六、测试POST的action
在控制器(Controller)中最常见的action是提交表单并POST一些数据。测试这个惊人的简单
public
function
testAddActionRedirectsAfterValidPost()
{
$albumTableMock
=
$this
->getMockBuilder(
'Album\Model\AlbumTable'
)
->disableOriginalConstructor()
->getMock();
$albumTableMock
->expects(
$this
->once())
->method(
'saveAlbum'
)
->will(
$this
->returnValue(null));
$serviceManager
=
$this
->getApplicationServiceLocator();
$serviceManager
->setAllowOverride(true);
$serviceManager
->setService(
'Album\Model\AlbumTable'
,
$albumTableMock
);
$postData
=
array
(
'title'
=>
'Led Zeppelin III'
,
'artist'
=>
'Led Zeppelin'
,
);
$this
->dispatch(
'/album/add'
,
'POST'
,
$postData
);
$this
->assertResponseStatusCode(302);
$this
->assertRedirectTo(
'/album'
);
}
运行phpunit得到以下结果
PHPUnit 3.7.13 by Sebastian Bergmann.
Configuration
read
from
/home/robert/www/zf2-tutorial/module/Album/test/phpunit
.xml
..
Time: 0 seconds, Memory: 10.75Mb
OK (2 tests, 9 assertions)
七、测试模型实体
现在我们知道了如何测试控制器(Controller),让我们转移到另一个应用程序重要的部分 - 模型实体。
这里我们要测试我们期望的实体的初始状态,我们可以从数组转换方法的参数,它有我们需要的所有的输入过滤。
在module/Album/test/AlbumTest/Model目录中创建AlbumTest.php文件,并输入以下代码:
<?php
namespace
AlbumTest\Model;
use
Album\Model\Album;
use
PHPUnit_Framework_TestCase;
class
AlbumTest
extends
PHPUnit_Framework_TestCase
{
public
function
testAlbumInitialState()
{
$album
=
new
Album();
$this
->assertNull(
$album
->artist,
'"artist" should initially be null'
);
$this
->assertNull(
$album
->id,
'"id" should initially be null'
);
$this
->assertNull(
$album
->title,
'"title" should initially be null'
);
}
public
function
testExchangeArraySetsPropertiesCorrectly()
{
$album
=
new
Album();
$data
=
array
(
'artist'
=>
'some artist'
,
'id'
=> 123,
'title'
=>
'some title'
);
$album
->exchangeArray(
$data
);
$this
->assertSame(
$data
[
'artist'
],
$album
->artist,
'"artist" was not set correctly'
);
$this
->assertSame(
$data
[
'id'
],
$album
->id,
'"id" was not set correctly'
);
$this
->assertSame(
$data
[
'title'
],
$album
->title,
'"title" was not set correctly'
);
}
public
function
testExchangeArraySetsPropertiesToNullIfKeysAreNotPresent()
{
$album
=
new
Album();
$album
->exchangeArray(
array
(
'artist'
=>
'some artist'
,
'id'
=> 123,
'title'
=>
'some title'
));
$album
->exchangeArray(
array
());
$this
->assertNull(
$album
->artist,
'"artist" should have defaulted to null'
);
$this
->assertNull(
$album
->id,
'"id" should have defaulted to null'
);
$this
->assertNull(
$album
->title,
'"title" should have defaulted to null'
);
}
public
function
testGetArrayCopyReturnsAnArrayWithPropertyValues()
{
$album
=
new
Album();
$data
=
array
(
'artist'
=>
'some artist'
,
'id'
=> 123,
'title'
=>
'some title'
);
$album
->exchangeArray(
$data
);
$copyArray
=
$album
->getArrayCopy();
$this
->assertSame(
$data
[
'artist'
],
$copyArray
[
'artist'
],
'"artist" was not set correctly'
);
$this
->assertSame(
$data
[
'id'
],
$copyArray
[
'id'
],
'"id" was not set correctly'
);
$this
->assertSame(
$data
[
'title'
],
$copyArray
[
'title'
],
'"title" was not set correctly'
);
}
public
function
testInputFiltersAreSetCorrectly()
{
$album
=
new
Album();
$inputFilter
=
$album
->getInputFilter();
$this
->assertSame(3,
$inputFilter
->
count
());
$this
->assertTrue(
$inputFilter
->has(
'artist'
));
$this
->assertTrue(
$inputFilter
->has(
'id'
));
$this
->assertTrue(
$inputFilter
->has(
'title'
));
}
}
- 所有的唱片属性是否都被初始化为NULL?
- 当我们调用exchangeArray()时唱片的属性是否能被正确的设置?
- 当$data数组中某个关键字不存在,是否默认采用NULL值?
- 我们是否可以得到一个复制模型的数组?
- 所有的元素是否都有输入过滤?
如果我们再次运行phpunit,我们会得到以下输出,确认我们的模型确实是正确的
PHPUnit 3.7.13 by Sebastian Bergmann.
Configuration
read
from
/var/www/zf2-tutorial/module/Album/test/phpunit
.xml
.......
Time: 0 seconds, Memory: 11.00Mb
OK (7 tests, 25 assertions)
八、测试模型表
Zend Framework 2单元测试教程的最后一步是为我们的模型表写测试
这个测试确保我们能得到一个唱片列表,或者根据ID获得一个唱片,以及我们可以在数据库中保存或者删除唱片。
为避免真实的与数据库交互,我们将使用模拟器代替部分内容
在module/Album/test/AlbumTest/Model目录中建立AlbumTableTest.php文件,并输入以下代码:
<?php
namespace
AlbumTest\Model;
use
Album\Model\AlbumTable;
use
Album\Model\Album;
use
Zend\Db\ResultSet\ResultSet;
use
PHPUnit_Framework_TestCase;
class
AlbumTableTest
extends
PHPUnit_Framework_TestCase
{
public
function
testFetchAllReturnsAllAlbums()
{
$resultSet
=
new
ResultSet();
$mockTableGateway
=
$this
->getMock(
'Zend\Db\TableGateway\TableGateway'
,
array
(
'select'
),
array
(),
''
,
false
);
$mockTableGateway
->expects(
$this
->once())
->method(
'select'
)
->with()
->will(
$this
->returnValue(
$resultSet
));
$albumTable
=
new
AlbumTable(
$mockTableGateway
);
$this
->assertSame(
$resultSet
,
$albumTable
->fetchAll());
}
}
public
function
testCanRetrieveAnAlbumByItsId()
{
$album
=
new
Album();
$album
->exchangeArray(
array
(
'id'
=> 123,
'artist'
=>
'The Military Wives'
,
'title'
=>
'In My Dreams'
));
$resultSet
=
new
ResultSet();
$resultSet
->setArrayObjectPrototype(
new
Album());
$resultSet
->initialize(
array
(
$album
));
$mockTableGateway
=
$this
->getMock(
'Zend\Db\TableGateway\TableGateway'
,
array
(
'select'
),
array
(),
''
,
false
);
$mockTableGateway
->expects(
$this
->once())
->method(
'select'
)
->with(
array
(
'id'
=> 123))
->will(
$this
->returnValue(
$resultSet
));
$albumTable
=
new
AlbumTable(
$mockTableGateway
);
$this
->assertSame(
$album
,
$albumTable
->getAlbum(123));
}
public
function
testCanDeleteAnAlbumByItsId()
{
$mockTableGateway
=
$this
->getMock(
'Zend\Db\TableGateway\TableGateway'
,
array
(
'delete'
),
array
(),
''
,
false
);
$mockTableGateway
->expects(
$this
->once())
->method(
'delete'
)
->with(
array
(
'id'
=> 123));
$albumTable
=
new
AlbumTable(
$mockTableGateway
);
$albumTable
->deleteAlbum(123);
}
public
function
testSaveAlbumWillInsertNewAlbumsIfTheyDontAlreadyHaveAnId()
{
$albumData
=
array
(
'artist'
=>
'The Military Wives'
,
'title'
=>
'In My Dreams'
);
$album
=
new
Album();
$album
->exchangeArray(
$albumData
);
$mockTableGateway
=
$this
->getMock(
'Zend\Db\TableGateway\TableGateway'
,
array
(
'insert'
),
array
(),
''
,
false
);
$mockTableGateway
->expects(
$this
->once())
->method(
'insert'
)
->with(
$albumData
);
$albumTable
=
new
AlbumTable(
$mockTableGateway
);
$albumTable
->saveAlbum(
$album
);
}
public
function
testSaveAlbumWillUpdateExistingAlbumsIfTheyAlreadyHaveAnId()
{
$albumData
=
array
(
'id'
=> 123,
'artist'
=>
'The Military Wives'
,
'title'
=>
'In My Dreams'
,
);
$album
=
new
Album();
$album
->exchangeArray(
$albumData
);
$resultSet
=
new
ResultSet();
$resultSet
->setArrayObjectPrototype(
new
Album());
$resultSet
->initialize(
array
(
$album
));
$mockTableGateway
=
$this
->getMock(
'Zend\Db\TableGateway\TableGateway'
,
array
(
'select'
,
'update'
),
array
(),
''
,
false
);
$mockTableGateway
->expects(
$this
->once())
->method(
'select'
)
->with(
array
(
'id'
=> 123))
->will(
$this
->returnValue(
$resultSet
));
$mockTableGateway
->expects(
$this
->once())
->method(
'update'
)
->with(
array
(
'artist'
=>
'The Military Wives'
,
'title'
=>
'In My Dreams'
),
array
(
'id'
=> 123)
);
$albumTable
=
new
AlbumTable(
$mockTableGateway
);
$albumTable
->saveAlbum(
$album
);
}
public
function
testExceptionIsThrownWhenGettingNonExistentAlbum()
{
$resultSet
=
new
ResultSet();
$resultSet
->setArrayObjectPrototype(
new
Album());
$resultSet
->initialize(
array
());
$mockTableGateway
=
$this
->getMock(
'Zend\Db\TableGateway\TableGateway'
,
array
(
'select'
),
array
(),
''
,
false
);
$mockTableGateway
->expects(
$this
->once())
->method(
'select'
)
->with(
array
(
'id'
=> 123))
->will(
$this
->returnValue(
$resultSet
));
$albumTable
=
new
AlbumTable(
$mockTableGateway
);
try
{
$albumTable
->getAlbum(123);
}
catch
(\Exception
$e
) {
$this
->assertSame(
'Could not find row 123'
,
$e
->getMessage());
return
;
}
$this
->fail(
'Expected exception was not thrown'
);
}
我们测试以下内容:
- 我们可以根据ID检索单个的唱片信息
- 我们可以删除唱片
- 我们可以保存新的唱片信息
- 我们可以更新已经存在的唱片信息
- 当我们检索一个不存在的唱片,我们可以得到一个异常
最后一次运行phpunit命令,得到以下的输出:
PHPUnit 3.7.13 by Sebastian Bergmann.
Configuration
read
from
/var/www/zf2-tutorial/module/Album/test/phpunit
.xml
.............
Time: 0 seconds, Memory: 11.50Mb
OK (13 tests, 34 assertions)
九、结论
在这个简短的教程中,我们得到了一些例子,如何测试Zend Framework 2 MVC 应用程度的不同部分。我们隐藏了设置测试环境,如何测试控制器(Controller)和action,如何处理失败的测试案例,如何配置服务管理器,测试模型实体和模型表。
本教程绝非一个明确的编写单元测试指导,只是一个小的敲门砖,帮助您开发更高质量的应用。- 单元测试一个ZF2应用程序
- ZF2.0用户向导 —— 3. 单元测试
- 学习zf2
- ZF2搭建
- ZF2.0用户向导 —— 2. 一个骨干框架应用
- 如何在的ServiceManager在ZF2注册一个Zend \ Log实例
- 应用程序打开一个应用程序
- 为ASP.NET MVC应用程序创建单元测试
- 为ASP.NET MVC应用程序创建单元测试
- 单元测试和集成测试业务应用程序
- 【JUnit实战】为应用程序Controller设计单元测试
- 怎么单独进行一个单元测试
- Junit单元测试的一个发现。
- 自己打造一个单元测试框架
- 一个Python单元测试的例子
- 做一个简单的单元测试
- 安装ZF2问题
- zf2 禁用layout
- OpenGL 矩阵变换(讲的太好了~!)
- [LeetCode] Minimum Depth of Binary Tree
- FreeMarker标签介绍:表达式及常用指令
- java线程阻塞中断和LockSupport的常见问题
- JAVA自定义数组工具类
- 单元测试一个ZF2应用程序
- PX与DIP互转
- hadoop、hbase、zookeeper整合kerberos,搭建安全平台
- 索尼公司发布第二财季财报:亏损7.85亿美元
- Android图片处理(Matrix,ColorMatrix)
- 软件管理
- LeetCode 119 Search for a Range
- UltraEdit 语法高亮 swf脚本文件 ActionScript 3.0
- 网络编程 TCP的那些事儿二