PHP单元测试,如何做?

时间:2022-08-26 14:59:22浏览次数:26

单元测试这个词相信大家也不陌生,本文将告诉你如何使用phpunit进行php单元测试,测试覆盖率等一些问题。单元测试用例越多,我们程序产生的BUG相对越少。那什么是单元测试呢?

什么是单元测试

单元测试又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。通过示例我们来了解怎么编写单元测试,怎么运行单元测试。

工程

创建一个测试工程,我们使有composer来作为phpunit启动器,不再使用官方的phpunit.phar文件了。

[meshell@phpunit]# composer init

更改生成的composer.json文件,添加以下依赖库。

"require": {

    "php": ">=7.4",

    "phpunit/phpunit": "^7",

    "friendsofphp/php-cs-fixer": "^2.3",

    "squizlabs/php_codesniffer": "^3.0",

    "php-coveralls/php-coveralls": "^2.0"

},

"autoload": {

    "psr-4": {

        "Test\\": "tests/src"

    }

}


安装依赖

[meshell@phpunit]# composer install

测试断言

phpunit提供50个断言函数。基本覆盖了所有的断言测试,还提供数十个注解功能。让我们来学习断言方法的使用。


namespace Test;


use PHPUnit\Framework\TestCase;


/**

 * Class AssertTest

 * @package Test`

 */

class AssertTest extends TestCase

{

    /**

     *

     */

    public function testDataType()

    {

        $this->assertIsInt(11); // 测试变量不是整型

        $this->assertIsArray([]); // 测试变量不是数组

        $this->assertIsFloat(1.1); // 测试变量不是符点型

        $this->assertIsBool(false); // 测试变量不是bool类型

        $this->assertIsCallable(function() {}); // 测试变量不是闭包

        $this->assertIsIterable(new \stdClass()); // 测试变量不是迭代器

    }


    /**

     *

     */

    public function testEqual()

    {

        $base = 111;

        $this->assertEquals($base, 2222); // 测试相等性

    }


    public function testContains()

    {

        $haystack = "test string contains";

        $this->assertContains("test", $haystack); // 测试字符包含关系

    }


    /**

     *

     */

    public function testArray()

    {

        $hash = [

            'body' => "hello world"

        ];

        $key = 'body';

        $this->assertArrayHasKey($key, $hash); // 测试key是否存在

    }

}

在建立测试单元时,测试类必须继承TestCase,测试方法必须是test开头,像上面的testDataType等方法。只需要在测试方法中调用你想测试的断言即可比如assertIsArray。查看全部的断言方法。

执行测试

使用命令来执行测试用例,你可以通过IDE或者phpunit.xml配置文件快速执行测试。我们将使用下面的命令执行上面的用例。

[meshell@phpunit]# php vendor/phpunit/phpunit/phpunit --no-configuration tests/src/AssertTest.php      

上面的命令将测试用例中的所有方法,不出意外你将会看到以下信息。

PHPUnit 7.5.11 by Sebastian Bergmann and contributors.


FF..                                                                4 / 4 (100%)


Time: 31 ms, Memory: 4.00 MB


There were 2 failures:


1) Test\AssertTest::testDataTypeFailed asserting that stdClass Object &000000001694dcb0000000001f4ce956 () is of type "iterable".


/Users/meshell/Projects/php/loocode-example/phpunit/tests/src/AssertTest.php:23


2) Test\AssertTest::testEqualFailed asserting that 2222 matches expected 111.


/Users/meshell/Projects/php/loocode-example/phpunit/tests/src/AssertTest.php:32


FAILURES!

Tests: 4, Assertions: 9, Failures: 2.

结果中会告诉你有多少个测试方法,多少断言, 多少个断言失败。比如上面的结果对应的就是4个测试, 9个断言, 2两个失败。

如果你想测试单个方法可以使用下面的命令

# testDataType 需要测试的方法

# Test\AssertTest 需要测试的类


[meshell@phpunit]# php vendor/phpunit/phpunit/phpunit --no-configuration --filter "/(::testDataType)( .*)?$/" Test\AssertTest tests/src/AssertTest.php

查看phpunit参数

测试依赖

通过测试依赖,可以将一个测试方法的结果传递给另外一个测试方法。这项功能我们不需要编写任何代码,只需要为主法加上一个注解名就可以。



namespace Test;



use PHPUnit\Framework\TestCase;


class DependenciesTest extends TestCase

{

    public function testEmpty()

    {

        $stack = [];

        $this->assertEmpty($stack);


        return $stack;

    }


    /**

     * @depends testEmpty

     */

    public function testPush(array $stack)

    {

        array_push($stack, 'foo');

        $this->assertEquals('foo', $stack[count($stack)-1]);

        $this->assertNotEmpty($stack);


        return $stack;

    }


    /**

     * @depends testPush

     */

    public function testPop(array $stack)

    {

        $this->assertEquals('foo', array_pop($stack));

        $this->assertEmpty($stack);

    }

}

我们为需要依赖的测试方法加上了@depends testPush注解,testPush、testEmpty都是被依赖的方法名。

[meshell@phpunit]# php vendor/phpunit/phpunit/phpunit --no-configuration tests/src/DependenciesTest.php


PHPUnit 7.5.11 by Sebastian Bergmann and contributors.


...                                                                 3 / 3 (100%)


Time: 72 ms, Memory: 4.00 MB


OK (3 tests, 5 assertions)

数据供给器

测试方法可以接受任意参数。这些参数由数据供给器方法additionProvider()提供。用@dataProvider标注来指定使用哪个数据供给器方法。

数据供给器方法必须声明为public,其返回值要么是一个数组,其每个元素也是数组;要么是一个实现了 Iterator 接口的对象,在对它进行迭代时每步产生一个数组。每个数组都是测试数据集的一部分,将以它的内容作为参数来调用测试方法。



namespace Test;



use PHPUnit\Framework\TestCase;


/**

 * Class ProviderTest

 * @package Test

 */

class ProviderTest extends TestCase

{


    /**

     * @dataProvider additionProvider

     */

    public function testAdd($a, $b, $expected)

    {

        $this->assertEquals($expected, $a + $b);

    }


    /**

     * @return array

     */

    public function additionProvider()

    {

        return [

            [0, 0, 0],

            [0, 1, 1],

            [1, 0, 1],

            [1, 1, 3]

        ];

    }

}


[meshell@phpunit]# php vendor/phpunit/phpunit/phpunit --no-configuration tests/src/ProviderTest.php

PHPUnit 7.5.11 by Sebastian Bergmann and contributors.


...F                                                                4 / 4 (100%)


Time: 28 ms, Memory: 4.00 MB


There was 1 failure:


1) Test\ProviderTest::testAdd with data set #3 (1, 1, 3)

Failed asserting that 2 matches expected 3.


/Users/meshell/Projects/php/loocode-example/phpunit/tests/src/ProviderTest.php:21


FAILURES!

Tests: 4, Assertions: 4, Failures: 1.

如果测试同时从@dataProvider方法和一个或多个@depends测试接收数据,那么来自于数据供给器的参数将先于来自所依赖的测试的。来自于所依赖的测试的参数对于每个数据集都是一样的。



namespace Test;



use PHPUnit\Framework\TestCase;


/**

 * Class ProviderTest

 * @package Test

 */

class ProviderTest extends TestCase

{

    ...


    public function provider()

    {

        return [['provider1'], ['provider2']];

    }


    public function testProducerFirst()

    {

        $this->assertTrue(true);

        return 'first';

    }


    public function testProducerSecond()

    {

        $this->assertTrue(true);

        return 'second';

    }


    /**

     * @depends testProducerFirst

     * @depends testProducerSecond

     * @dataProvider provider

     */

    public function testConsumer()

    {

        $this->assertEquals(

            ['provider1', 'first', 'second'],

            func_get_args()

        );

    }

}

[meshell@phpunit]# php vendor/phpunit/phpunit/phpunit --no-configuration tests/src/ProviderTest.php

PHPUnit 7.5.11 by Sebastian Bergmann and contributors.

…F…F                                                            8 / 8 (100%)

Time: 51 ms, Memory: 4.00 MB

There were 2 failures:

1) Test\ProviderTest::testAdd with data set #3 (1, 1, 3)

Failed asserting that 2 matches expected 3.

/Users/meshell/Projects/php/loocode-example/phpunit/tests/src/ProviderTest.php:21

2) Test\ProviderTest::testConsumer with data set #1 ('provider2')

Failed asserting that two arrays are equal.

--- Expected

+++ Actual

@@ @@

Array (

0 => 'provider1'


0 => 'provider2'

1 => 'first'

2 => 'second'

)

/Users/meshell/Projects/php/loocode-example/phpunit/tests/src/ProviderTest.php:63

FAILURES!

Tests: 8, Assertions: 8, Failures: 2.


Note:如果一个测试依赖于另外一个使用了数据供给器的测试,仅当被依赖的测试至少能在一组数据上成功时,依赖于它的测试才会运行。使用了数据供给器的测试,其运行结果是无法注入到依赖于此测试的其他测试中的。

所有的数据供给器方法的执行都是在对setUpBeforeClass静态方法的调用和第一次对setUp方法的调用之前完成的。因此,无法在数据供给器中使用创建于这两个方法内的变量。这是必须的,这样 PHPUnit 才能计算测试的总数量。

异常测试

使用expectException方法或者@expectedException注解可以进行代码的异常测试。


namespace Test;



use InvalidArgumentException;

use PHPUnit\Framework\TestCase;


class ExceptionTest extends TestCase

{

    public function testException()

    {

        $this->expectException(InvalidArgumentException::class);

    }


    /**

     * @expectedException InvalidArgumentException

     */

    public function testAException()

    {

        throw new InvalidArgumentException("无效的参数", 500);

    }

}

[meshell@phpunit]# php vendor/phpunit/phpunit/phpunit --no-configuration tests/src/ExceptionTest.php

PHPUnit 7.5.11 by Sebastian Bergmann and contributors.

F.                                                                  2 / 2 (100%)

Time: 28 ms, Memory: 4.00 MB

There was 1 failure:

1) Test\ExceptionTest::testException

Failed asserting that exception of type "InvalidArgumentException" is thrown.

FAILURES!

Tests: 2, Assertions: 2, Failures: 1.

可以看执行之后的结果,第二个方法抛出了异常被断言捕获。除了expectException、@expectedException还有测试code,message的方法和注解expectExceptionCode、expectExceptionMessage、@expectedExceptionCode、@expectedExceptionMessage。官方后期会移除expectException和@expectedException的支持需要使用明确的code,message测试。


错误测试

默认情况下,PHPUnit将测试在执行中触发的PHP错误、警告、通知都转换为异常。利用这些异常,就可以测试。比如说,预期测试将触发PHP错误比如加载了一个不存在的文件。



namespace Test;



use PHPUnit\Framework\Error\Error;

use PHPUnit\Framework\TestCase;


class ErrorTest extends TestCase

{

    /**

     * @expectedException Error

     */

    public function testNotFile()

    {

        include "not.file.php";

    }

}


[meshell@phpunit]# php vendor/phpunit/phpunit/phpunit --no-configuration tests/src/ErrorTest.php

PHPUnit 7.5.11 by Sebastian Bergmann and contributors.

E                                                                   1 / 1 (100%)

Time: 27 ms, Memory: 4.00 MB

There was 1 error:

1) Test\ErrorTest::testNotFile

include(not.file.php): failed to open stream: No such file or directory

/Users/meshell/Projects/php/loocode-example/phpunit/tests/src/ErrorTest.php:17

/Users/meshell/Projects/php/loocode-example/phpunit/tests/src/ErrorTest.php:17

ERRORS!

Tests: 1, Assertions: 0, Errors: 1.

相信应该已经学会了怎么使用phpunit来进行单元测试了吧