Discuz教程网

PHP单元测试利器 PHPUNIT深入用法

[复制链接]
authicon dly 发表于 2011-3-8 17:40:03 | 显示全部楼层 |阅读模式
1、markTestSkipped和markTestIncomplete
  在phpunit中,有两个有用的方法markTestSkipped和markTestIncomplete。它们能允许你编写的单元测试中不单是只有通过和失败两种结果。markTestSkipped能让PHPUNIT不去执行某个已经编写好的测试方法。举个例子说明,比如下面的程序:
  1. <?php
  2. public function testThisMightHaveADb()
  3. {
  4. $myObject->createObject();
  5. try {
  6. $db = new Database();
  7. $this->assertTrue($db->rowExists());
  8. } catch (DatabseException $e) {
  9. $this->markTestSkipped('This test was skipped because there was a database problem');
  10. }
  11. }
  12. ?>
复制代码

  在上面的程序中,是一个连接数据库后,判断数据是否存在的测试方法,但如果考虑数据库的连接异常的话,则应该在抛出异常时,使用markTestSkipped指出该测试方法应该是被忽略的,因为出现了异常,而注意的时,此时有可能你写的代码是正确的,只不过是出现了异常而已,这样phpunit在输出时就不会只是简单的输出fail。
  而markTestIncomplete也有点类似,但有点不同的是,它是当开发者在编写一个未完成的测试方法时使用的,标记出某个测试方法还没编写完成,同样测试结果也不会是fail,只是告诉phpunit这个测试方法还没编写完成而已,例子如下:
  1. <?php
  2. public function testAreNotEnoughHours()
  3. {
  4. $this->markTestIncomplete("There aren't enough hours in the day to have my tests go green");
  5. $trueVariable = true;
  6. $this->assertTrue($trueVariable);
  7. }
  8. ?>
复制代码

  2、更深入了解phpunit中的断言
  在上一篇文章中,已经基本讲解了一些基本的phpunit中的断言的使用,这里以一个例子,下面是一个类的代码:
  1. <?php
  2. class Testable
  3. {
  4. public $trueProperty = true;
  5. public $resetMe = true;
  6. public $testArray = array(
  7. 'first key' => 1,
  8. 'second key' => 2
  9. );
  10. private $testString = "I do love me some strings";
  11. public function __construct()
  12. {
  13. }
  14. public function addValues($valueOne,$valueTwo) {
  15. return $valueOne+$valueTwo;
  16. }
  17. public function getTestString()
  18. {
  19. return $this->testString;
  20. }
  21. }
  22. ?>
复制代码

  我们编写的单元测试代码初步的框架如下:
  1. <?php
  2. class TestableTest extends PHPUnit_Framework_TestCase
  3. {
  4. private $_testable = null;
  5. public function setUp()
  6. {
  7. $this->_testable = new Testable();
  8. }
  9. public function tearDown()
  10. {
  11. $this->_testable = null;
  12. }
  13. /** test methods will go here */
  14. }
  15. ?>
复制代码

  在上一篇文章中,已经介绍了setUp方法和tearDown方法,这里的setUp方法中,建立了Testable()实例并保存在变量$_testable中,而在tearDown方法中,销毁了该对象。
  接下来,开始编写一些断言去测试,首先看assertTrue和assertFalase:

  1. <?php
  2. public function testTruePropertyIsTrue()
  3. {
  4. $this->assertTrue($this->_testable->trueProperty,"trueProperty isn't true");
  5. }
  6. public function testTruePropertyIsFalse()
  7. {
  8. $this->assertFalse($this->_testable->trueProperty, "trueProperty isn't false");
  9. }
  10. ?>
复制代码

在上一篇文章中已经介绍过assertTrue和assertFalse了,这里留意一下其中的第二个参数,其含义是,当该断言的测试不通过时,自定义的显示信息。比如在这个测试方法中,当trueProperty不为真值时,将显示“trueProperty isn't true”的信息。
  接下来再看下在数值方面上phpunit的断言使用实例:
  1. <?php
  2. public function testValueEquals()
  3. {
  4. $valueOne = 4;
  5. $valueTwo = 2;
  6. $this->assertEquals($this->_testable->addValues($valueOne,$valueTwo),6);
  7. }
  8. public function testValueGreaterThan()
  9. {
  10. $valueOne = 4;
  11. $valueTwo = 2;
  12. $this->assertGreaterThan($valueTwo,$valueOne);
  13. }
  14. public function testLessThanOrEqual()
  15. {
  16. $valueOne = 4;
  17. $valueTwo = 2;
  18. $this->assertLessThanOrEqual($valueTwo,$valueOne);
  19. }
  20. public function testAreObjectsEqual()
  21. {
  22. $testTwo = new Testable();
  23. $this->_testable->resetMe = false;
  24. $this->assertEquals($this->_testable,$testTwo);
  25. }
  26. ?>
复制代码

  其中,assertEquals为判断是否相等,assertGreaterThan为判断是否大于,assertLessThanOrEqual判断是否小于或等于,而assertEquals这里要注意一下,它还可以用来判断两个对象是否相等,比如这里就判断了$testTwo这个Testable类的实例是否和新设置的resetMe这个对象相等。
  除了在数值方面的断言外,在字符方面还有一些很多断言的功能,看下面的代码:
  1. <?php
  2. public function testStringEnding()
  3. {
  4. $testString = $this->_testable->getTestString();
  5. $this->assertStringEndsWith('frood',$testString);
  6. }
  7. public function testStringStarts()
  8. {
  9. $testString = $this->_testable->getTestString();
  10. $this->assertStringStartsWith('hoopy',$testString);
  11. }
  12. public function testEqualFileContents()
  13. {
  14. $this->assertStringEqualsFile('/path/to/textfile.txt','foo');
  15. }
  16. public function testDoesStringMatchFormat()
  17. {
  18. $testString = $this->_testable->getTestString();
  19. $this->assertStringMatchesFormat('%s',$testString);
  20. }
  21. ?>
复制代码

  其中, assertStringStartsWith断言是判断字符串是否以指定的字符串开头,assertStringEndsWith断言判断字符串是否以指定的字符串结尾。assertStringEqualsFile断言判断给定的文件中是否含有指定的字符,比如这里就判断textfile.txt这个文件中是否包含字符串foo。
  而assertStringMatchesFormat可以让用户指定匹配的模式去判断一个字符串是否符合要求,如 $this->assertStringMatchesFormat('%s',$testString);
  这里则判断$testString是否是字符串类型,具体的可以参考phpunit手册。
  再来看如下的代码:
  1. <?php
  2. public function testStringIsNotNull()
  3. {
  4. $notANull = “i'm not a null!”;
  5. $this->assertNull($notANull);
  6. }
  7. public function testStringIsSame()
  8. {
  9. $numberAsString = '1234';
  10. $this->assertSame(1234,$numberAsString);
  11. }
  12. ?>
复制代码

  其中assertNull判断某个变量是否为null,而assertSame则严格判断两个变量是否同一个类型,尽管在PHP中是弱类型语言,但这里通过assertSame还是能判断出$numberAsString为字符串类型,跟期望的1234数字类型不匹配,所以测试不能通过。
  最后我们来看一下平常可能不大常用的断言,但又可能对你的单元测试工作十分有帮助的,先看代码如下:
  1. <?php
  2. public function testArrayKeyExists()
  3. {
  4. $this->assertArrayHasKey('first key',$this->_testable->testArray);
  5. }
  6. public function testAttributeExists()
  7. {
  8. $this->assertClassHasAttribute('resetMe',get_class($this->_testable));
  9. }
  10. public function testFileIsReal()
  11. {
  12. $this->assertFileExists('/path/to/file.txt');
  13. }
  14. public function testIsInstance()
  15. {
  16. $this->assertInstanceOf('OtherClass',$this->_testable);
  17. }
  18. <?php
  19. public function testDoesMatchRegex()
  20. {
  21. $testString = $this->_testable->getTestString();
  22. $this->assertRegExp('/[a-z]+/',$testString);
  23. }
  24. ?>
复制代码

  代码中第一个断言assertArrayHasKey,是用来检查一个数组中是否每个键值都是存在的,比如我们的数组中,“firstkey”这个值是有键1与其对应的,所以测试能通过。而assertClassHasAttribute则能判断某个类是否有相应的属性,这个例子中测试也能通过;
  而assertFileExists则判断在本地文件系统中是否存在指定的文件。而assertInstanceOf则判断某个你正在创建的对象是否为某个类的实例。assertRegExp相信大家都知道,这个是判断某个字符串中是否与给定的正则表达式相匹配。
  总结
  本文进一步探讨了PHPUNIT中一些重要的方法和断言,PHPUNIT中还有大量丰富的断言,对提高单元测试十分有帮助,具体的请参考PHPUNIT用户手册。





上一篇:PHP单元测试利器 PHPUNIT初探
下一篇:PHP数组交集的优化代码分析
authicon  楼主| dly 发表于 2011-3-8 17:43:22 | 显示全部楼层
在本文中,笔者将为大家介绍phpunit中的两个高级概念和用法,尽管它不一定在你的日常单元测试中都用到,但理解和学会它们的用法对学习phpunit还是十分重要的。
  Phpunit中的Annotations
  如果有其他编程语言经验的开发者,应该对Annotations(注解)不陌生,其实在phpunit中,一个简单的如下面的一段注释也可以认为是Annotations:
  1. <?php
  2. class MyTestClass extends PHPUnit_Framework_TestCase
  3. {
  4. /**
  5. * Testing the answer to “do you love unit tests?”
  6. */
  7. public function testDoYouLoveUnitTests()
  8. {
  9. $love = true;
  10. $this->assertTrue($love);
  11. }
  12. }
  13. ?>
复制代码

  可以看到,其实一段以/** **/为标记的文字,就可以认为是一种Annotations,但Annotations其实不单单是简单的注释,它是与一个程序元素相关联信息或者元数据的标注,它不影响程序的运行,但相关的软件工具或框架能够将其转换成特殊的元数据标记,以方便开发者以更少的代码去提高效率(比如通过。如果你熟悉Java,则会发现在Java SE 5中及象Spring等框架中,都大量使用了Annotations。
  然而,由于php并不象Java那样是编译性语言,因此本身缺乏去解析Annotations的机制,但幸好phpunit去提供了这样的功能,我们以下面的代码为例:
  1. <?php
  2. class MyMathClass
  3. {
  4. /**
  5. * Add two given values together and return sum
  6. */
  7. public function addValues($a,$b)
  8. {
  9. return $a+$b;
  10. }
  11. }
  12. ?>
复制代码

  上面的只是一个简单的加法的例子,为此,我们使用Annotations去编写一个单元测试,在上两篇文章中,我们采用的是手工编写单元测试的方法,而本文中,将介绍使用phpunit命令行的方法,自动生成单元测试的框架,方法如下:
  首先把上面的类保存为MyMathClass.php,然后在命令行下运行如下命令:
phpunit –skeleton-test MyMathClass
  这时phpunit会自动生成如下的框架单元测试代码:

  1. <?php
  2. require_once '/path/to/MyMathClass.php';
  3. /**
  4. * Test class for MyMathClass.
  5. * Generated by PHPUnit on 2011-02-07 at 12:22:07.
  6. */
  7. class MyMathClassTest extends PHPUnit_Framework_TestCase
  8. {
  9. /**
  10. * @var MyMathClass
  11. */
  12. protected $object;
  13. /**
  14. * Sets up the fixture, for example, opens a network connection.
  15. * This method is called before a test is executed.
  16. */
  17. protected function setUp()
  18. {
  19. $this->object = new MyMathClass;
  20. }
  21. /**
  22. * Tears down the fixture, for example, closes a network connection.
  23. * This method is called after a test is executed.
  24. */
  25. protected function tearDown()
  26. {
  27. }
  28. /**
  29. * @todo Implement testAddValues().
  30. */
  31. public function testAddValues()
  32. {
  33. // Remove the following lines when you implement this test.
  34. $this->markTestIncomplete(
  35. 'This test has not been implemented yet.'
  36. );
  37. }
  38. }
  39. ?>
复制代码

  可以看到,phpunit为我们生成的单元测试代码自动引入了原来的MyMathClass.php,同时也生成了setUp和tearDown方法,但唯一的核心单元测试代码是留给了我们编写。如果想在这个基础上更快速的生成我们想要的单元测试代码,要如何实现呢?没错,就是使用annotations!我们可以在原来的MyMathClass.php中加入如下的annotations。

  1. <?php
  2. class MyMathClass
  3. {
  4. /**
  5. * Add two given values together and return sum
  6. * @assert (1,2) == 3
  7. */
  8. public function addValues($a,$b)
  9. {
  10. return $a+$b;
  11. }
  12. }
  13. ?>
复制代码

  然后再象上述一样在命令行运行:
phpunit –skeleton-test MyMathClass
  这个时候会为我们生成如下的单元测试代码:

  1. <?php
  2. /**
  3. * Generated from @assert (1,2) == 3.
  4. */
  5. public function testAddValues()
  6. {
  7. $this->assertEquals(
  8. 3,
  9. $this->object->addValues(1,2)
  10. );
  11. }
  12. ?>
复制代码

  看到了么?我们在原有的类中加入了注解@assert(1,2)==3,则phpunit自动为我们生成了正确的单元测试代码。当然,可以参考phpunit手册,学习到更多的关于@assert注解使用的规则。
  下面再举一个例子来讲解annotations。假设我们的程序中的一个方法,只是仅需要数据的输入,并且不依赖XML或者数据库提供数据源,则为了测试这个方法,我们可能想到的一个方法是在程序中设置一个测试数据集去测试,但这里介绍一个比较简单的方法,就是使用注解@dataProvider,修改MyMathClass.php如下:

  1. <?php
  2. /**
  3. * Data provider for test methods below
  4. */
  5. public static function provider()
  6. {
  7. return array(
  8. array(1,2,3),
  9. array(4,2,6),
  10. array(1,5,7)
  11. );
  12. }
  13. /**
  14. * Testing addValues returns sum of two values
  15. * @dataProvider provider
  16. */
  17. public function testAddValues($a,$b,$sum)
  18. {
  19. $this->assertEquals(
  20. $sum,
  21. $this->object->addValues($a,$b)
  22. );
  23. }
  24. ?>
复制代码

  可以看到,这里使用了注解@dataProvider,指明了测试用例的数据提供者是由provider方法返回的一个数组。所以在单元测试时,数组中的第0个元素则会赋值给$a,第1个元素则会赋值给b,第3个元素则会赋值给sum,可以看到,上面的第3个数组提供的数据是不能通过单元测试的,因为1+5不等于7。
  此外,这里还简单介绍两个常用的annotations,比如@expectedException注解可以测试代码中是否正确抛出了异常,比如:

  1. <?phprequire_once 'PHPUnit/Framework.php';
  2. class ExceptionTest extends PHPUnit_Framework_TestCase{
  3. /**
  4. * @expectedException InvalidArgumentException */
  5. public function testException() {
  6. }
  7. }
  8. ?>
复制代码

  这里就用注解的方法表示testException中必须抛出的异常类型为InvalidArgumentException。
  另外一个是@cover注解。它的作用是标识phpunit只为类中的哪些方法或作用域生成测试代码,比如:

  1. /**
  2. * @covers SampleClass::publicMethod
  3. * @covers SampleClass::<!public>
  4. * @covers HelperClass<extended>
  5. */
  6. public function testMethod()
  7. {
  8. $result = SampleClass::method();
  9. }
复制代码

  则phpunit只为SampleClass类中的publicMethod方法、SampleClass类中的所有非public声明的方法和HelperClass类或者它的其中一个父类产生单元测试代码。
authicon  楼主| dly 发表于 2011-3-8 17:44:43 | 显示全部楼层
Phpunit中的Mocking
  在介绍Mocking前,先来看下为什么要使用Mocking。举一个数据库查询的例子,比如在某个应用中,如果要测试一个数据库的应用,但假如这个数据库的测试要耗费很多资源以及编写很复杂的单元测试的代码的话,可以尝试使用Mocking技术。举例说明如下:

  1. <?php
  2. class Database
  3. {
  4. public function reallyLongTime()
  5. {
  6. $results = array(
  7. array(1,'test','foo value')
  8. );
  9. sleep(100);
  10. return $results;
  11. }
  12. }
  13. ?>
复制代码


  在上面这个例子中,我们模拟了一个数据库的操作,认为它需要运行很长时间。接下来我们编写其单元测试代码如下:

  1. <?php
  2. require_once '/path/to/Database.php';
  3. class DatabaseTest extends PHPUnit_Framework_TestCase
  4. {
  5. private $db = null;
  6. public function setUp()
  7. {
  8. $this->db = new Database();
  9. }
  10. public function tearDown()
  11. {
  12. unset($this->db);
  13. }
  14. /**
  15. * Test that the "really long query" always returns values
  16. */
  17. public function testReallyLongReturn()
  18. {
  19. $mock = $this->getMock('Database');
  20. $result = array(
  21. array(1,'foo','bar test')
  22. );
  23. $mock->expects($this->any())
  24. ->method('reallyLongTime')
  25. ->will($this->returnValue($result));
  26. $return = $mock->reallyLongTime();
  27. $this->assertGreaterThan(0,count($return));
  28. }
  29. }
  30. ?>
复制代码


  注意看这段代码中有趣的地方,这里,使用了phpunit中的getMock对象方法,这里实际上是模拟生成一个Database类的“伪实例”了,这里生成了$mock这个mock对象实例,以方便接着的单元测试中用到。接下来的这三行代码:

  1. $mock->expects($this->any())
  2. ->method('reallyLongTime')
  3. ->will($this->returnValue($result));
复制代码


  它们的含义为:无论方法reallyLongtime执行了多长时间,始终最后会直接返回$result这个数组的结果。这样,你就可以通过mocking技术很轻易地去实现在单元测试中,绕过某些复杂的逻辑部分,而节省大量的宝贵时间提高测试效率。
  下面的这个例子,讲解的是Mocking技术中的更高级用法Mockbuilder。依然以上面的例子说明:

  1. <?php
  2. public function testReallyLongRunBuilder()
  3. {
  4. $stub = $this->getMockBuilder('Database')
  5. ->setMethods(array(
  6. 'reallyLongTime'
  7. ))
  8. ->disableAutoload()
  9. ->disableOriginalConstructor()
  10. ->getMock();
  11. $result = array(array(1,'foo','bar test'));
  12. $stub->expects($this->any())
  13. ->method('reallyLongTime')
  14. ->will($this->returnValue($result));
  15. $this->assertGreaterThan(0,count($return));
  16. }
  17. ?>
复制代码


  通过使用Mockbuilder,我们可以不用通过构造函数的方法去初始化一个mock对象。这段代码跟上一段代码的功能其实是一样的,只不过留意一下新的两个方法: disableAutoload和disableOriginalConstructor,其功能分别是禁止使用php的内置的autoload初始构造方法和禁止调用该类原有的构造函数。最后再看一个例子:

  1. <?php
  2. /**
  3. * Testing enforcing the type to "array" like the "enforceTypes"
  4. * method does via type hinting
  5. */
  6. public function ttestReallyLongRunBuilderConstraint()
  7. {
  8. $stub = $this->getMock('Database',array('reallyLongTime'));
  9. $stub->expects($this->any())
  10. ->method('reallyLongTime')
  11. ->with($this->isType('array'));
  12. $arr = array('test');
  13. $this->assertTrue($stub-> reallyLongTime ($arr));
  14. }
  15. ?>
复制代码

  在这里,我们使用了with方法,其中这个方法中指定了要传入的参数类型为array数组类型,最后这个断言是通过了,因为返回的的确是数组类型。
authicon 乐娃娃 发表于 2011-5-8 03:59:43 | 显示全部楼层
这个还是不错的!
authicon melody0721 发表于 2011-5-11 07:59:38 | 显示全部楼层
这个不错呀
authicon 咫尺天 发表于 2011-5-18 21:00:00 | 显示全部楼层
顶的就是你
authicon summmer 发表于 2011-5-20 00:59:42 | 显示全部楼层
哈哈  收了你
authicon 21585151 发表于 2011-5-22 14:59:48 | 显示全部楼层
这个贴不错!!!
authicon forever21 发表于 2011-6-16 13:00:02 | 显示全部楼层
看看  好像不错
authicon lilac_yao 发表于 2011-6-17 04:00:05 | 显示全部楼层
哦哦,发财了啊,看到好东西啦
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1314学习网 ( 浙ICP备10214163号 )

GMT+8, 2025-5-3 15:15

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表