作为一个开发人员,必需对自己的代码负责,而一个行之有效的方法就是编写单元测试,虽然说现在的变化很快,可能今天订好的接口,明天就会因为新需求的介入而面临重构的问题,当然这个问题只要软件在发展都是不可避免的事情,所以导致了单元测试的成本升高,而造成很多滞后问题,这就会造成:虽然一直在提倡测试先行,测试驱动的开发模式不能成为长久的行之有效的方案。但是单元测试,果然很重要。
从Java开发转向PHP(虽然java还没有机会进入公司实践),正是因为PHP的快速开发的特点,很少有机会提及说要记得写单元测试这种话。所以这块就感觉的更加的匮乏,当然也有可能跟我目前的公司开发模式有关系。
好了话不多说了,进入正题。
PHP中的单元测试,有一个类库PHPUnit现在最新版本应该有3.5了(我的测试环境:PHP的版本要求5.2.5,pear的版本要求1.9.1,phpunit:3.4.15),PHPUnit的安装是依赖pear安装的,安装的方式我就不说了,我相信在网上可以找到很多行之有效的方法。
安装好之后(为了方便我配置了环境),命令行下就可以通过输入 phpunit来查看一些提供的命令和版本信息了。
其中的几个命令选项如:coverage-开头的是因为我在本机上安装了XDEBUG库出现的,我不的不说xdebug确实是一个很好很棒的工具,能分析代码的性能,也能测试代码覆盖率。xdebug的安装我也不赘述了,google出同样有很多,我之前也有写了一些。
单元测试说简单也简单,主要涉及到两个类,分别是单元测试类,和单元测试的套件类:(以下实例调试环境为Zend studio 8.0),在使用单元测试之前,你需要将你的单元测试类库包含到你的工程中,右击工程->build IncludePath
- 单元测试类PHPUnit_Framework_TestCase
创建一个class,继承PHPUnit_Framework_TestCase,
1: class MyTest extends PHPUnit_Framework_TestCase {
2:
3: protected function setUp() {
4: parent::setUp();
5: }
6:
7: protected function tearDown() {
8: parent::tearDown();
9: }
10: }
setUp和tearDown分别是每个单元测试方法执行的前置和后置操作。
单元测试的测试方法,都是以test开头进行测试:如:
1: public function testGetName() {
2: $this->assertTrue('myname' == 'myname');
3: }
4:
5: public function testGetEmail() {
6: $this->assertEquals('myname', 'myname1');
7: }
分别写两个测试方法:执行会发现:
当正确的时候,和在该方法的前面显示一个绿色的钩钩,而如果是失败的方法,则会显示一个红叉叉,而上面的那条状态颜色就会是红色的。
而如果我这个单元测试类中所有方法都测试通过的时候,则会显示绿色的通行颜色:
单元测试就是这么简单的,对于他里面有啥测试方法,可以查看PHPUnit/frameWork下的testCase类,可以看到很多验证的方法提供,我上面用到的只是其中的两个而已。
2. 单元测试套件PHPUnit_Framework_TestSuite
套件,名其曰就是多个测试的集合,可以同时测试多个测试类。
测试套件有两种写法,两种写法都可以在命令行下运行:
1)以继承的方式:
将上面的单元测试做一个套件:
1: require_once 'PHPUnit\Framework\TestSuite.php';
2: require_once 'MyTest.php';
3: class MyTestSuite extends PHPUnit_Framework_TestSuite {
4:
5: public function __construct() {
6: $this->setName('MyTestSuite');
7: $this->addTestSuite('MyTest');
8: }
9:
10: public static function suite() {
11: return new self();
12: }
13: }
执行:
这种方式也是zend studio创建的时候默认提供的,testSuite中提供的方法就是你的套件的方法了,这个方式通常比较简单。但是对于testSuite中需要很多注意的。这个也可以从他的源代码中看出来,
2)以调用的方式
还是将上面的测试类做测试:
1: require_once('MyTest.php');
2: class MyTestSuite2 {
3: static public function main() {
4: PHPUnit_TextUI_TestRunner::run(self::suite());
5: }
6:
7: static public function suite() {
8: $suite = new PHPUnit_Framework_TestSuite('MyTestSuite');
9: $suite->addTestSuite('MyTest');
10: return $suite;
11: }
12: }
这种方式在Zend Studio中运行会有问题(我本地测试是这样),但是用命令行运行是可以的:
说明:命令行下运行:
. 代表测试方法通过
I 代表该方法没有实现
F 代表方法测试失败
E 代表方法测试有错误输出
通常往一个套件中添加测试方法是通过addTestSuite和addTest两个来完成:实现代码如下:
1: /**
2: * Adds the tests from the given class to the suite.
3: *
4: * @param mixed $testClass
5: * @throws InvalidArgumentException
6: */
7: public function addTestSuite($testClass)
8: {
9: if (is_string($testClass) && class_exists($testClass)) {
10: $testClass = new ReflectionClass($testClass);
11: }
12:
13: if (!is_object($testClass)) {
14: throw PHPUnit_Util_InvalidArgumentHelper::factory(
15: 1, 'class name or object'
16: );
17: }
18:
19: if ($testClass instanceof PHPUnit_Framework_TestSuite) {
20: $this->addTest($testClass);
21: }
22:
23: else if ($testClass instanceof ReflectionClass) {
24: $suiteMethod = FALSE;
25:
26: if (!$testClass->isAbstract()) {
27: if ($testClass->hasMethod(PHPUnit_Runner_BaseTestRunner::SUITE_METHODNAME)) {
28: $method = $testClass->getMethod(
29: PHPUnit_Runner_BaseTestRunner::SUITE_METHODNAME
30: );
31:
32: if ($method->isStatic()) {
33: $this->addTest(
34: $method->invoke(NULL, $testClass->getName())
35: );
36:
37: $suiteMethod = TRUE;
38: }
39: }
40: }
41:
42: if (!$suiteMethod && !$testClass->isAbstract()) {
43: $this->addTest(new PHPUnit_Framework_TestSuite($testClass));
44: }
45: }
46:
47: else {
48: throw new InvalidArgumentException;
49: }
50: }
通过addTestSuite添加的,应该是一个testSuite或是testCase或是一个类中含有名为suite的静态方法方可。
我们再来看看addTest:
1: /**
2: * Adds a test to the suite.
3: *
4: * @param PHPUnit_Framework_Test $test
5: * @param array $groups
6: */
7: public function addTest(PHPUnit_Framework_Test $test, $groups = array())
8: {
9: $class = new ReflectionClass($test);
10:
11: if (!$class->isAbstract()) {
12: $this->tests[] = $test;
13: $this->numTests = -1;
14:
15: if ($test instanceof PHPUnit_Framework_TestSuite &&
16: empty($groups)) {
17: $groups = $test->getGroups();
18: }
19:
20: if (empty($groups)) {
21: $groups = array('__nogroup__');
22: }
23:
24: foreach ($groups as $group) {
25: if (!isset($this->groups[$group])) {
26: $this->groups[$group] = array($test);
27: } else {
28: $this->groups[$group][] = $test;
29: }
30: }
31: }
32: }
这个方法就比较简单了,就是从testSuite中拿到注册的test方法。
而对于testSuite的构造方法,对齐传入的参数的规定也是比较复杂:
1: /**
2: * Constructs a new TestSuite:
3: *
4: * - PHPUnit_Framework_TestSuite() constructs an empty TestSuite.
5: *
6: * - PHPUnit_Framework_TestSuite(ReflectionClass) constructs a
7: * TestSuite from the given class.
8: *
9: * - PHPUnit_Framework_TestSuite(ReflectionClass, String)
10: * constructs a TestSuite from the given class with the given
11: * name.
12: *
13: * - PHPUnit_Framework_TestSuite(String) either constructs a
14: * TestSuite from the given class (if the passed string is the
15: * name of an existing class) or constructs an empty TestSuite
16: * with the given name.
17: *
18: * @param mixed $theClass
19: * @param string $name
20: * @throws InvalidArgumentException
21: */
22: public function __construct($theClass = '', $name = '')
23: {
24: $argumentsValid = FALSE;
25:
26: if (is_object($theClass) &&
27: $theClass instanceof ReflectionClass) {
28: $argumentsValid = TRUE;
29: }
30:
31: else if (is_string($theClass) && $theClass !== ''
32: && class_exists($theClass, FALSE)) {
33: $argumentsValid = TRUE;
34:
35: if ($name == '') {
36: $name = $theClass;
37: }
38:
39: $theClass = new ReflectionClass($theClass);
40: }
41:
42: else if (is_string($theClass)) {
43: $this->setName($theClass);
44: return;
45: }
46:
47: if (!$argumentsValid) {
48: throw new InvalidArgumentException;
49: }
50:
51: $filename = $theClass->getFilename();
52:
53: if (strpos($filename, 'eval()') === FALSE) {
54: PHPUnit_Util_Filter::addFileToFilter(realpath($filename), 'TESTS');
55: }
56:
57: if ($name != '') {
58: $this->setName($name);
59: } else {
60: $this->setName($theClass->getName());
61: }
62:
63: $constructor = $theClass->getConstructor();
64:
65: if ($constructor !== NULL &&
66: !$constructor->isPublic()) {
67: $this->addTest(
68: self::warning(
69: sprintf(
70: 'Class "%s" has no public constructor.',
71:
72: $theClass->getName()
73: )
74: )
75: );
76:
77: return;
78: }
79:
80: $names = array();
81:
82: foreach ($theClass->getMethods() as $method) {
83: if (strpos($method->getDeclaringClass()->getName(), 'PHPUnit_') !== 0) {
84: $this->addTestMethod($theClass, $method, $names);
85: }
86: }
87:
88: if (empty($this->tests)) {
89: $this->addTest(
90: self::warning(
91: sprintf(
92: 'No tests found in class "%s".',
93:
94: $theClass->getName()
95: )
96: )
97: );
98: }
99: }
我的第二种方法也是通过这中构造函数的方式来实现。
好了··就这样··有啥疑问或是纰漏的地方欢迎交流