我该如何在PHP中编写单元测试?

100

我已经看过很多关于它们有多好的文章,但由于某些原因,我似乎无法弄清楚应该如何测试某个东西。是否有人可以发一段示例代码以及他们如何进行测试?如果不麻烦的话 :)


你可以通过调整编码风格来提高单元测试的效果。我建议浏览Google Testing Blog,特别是有关编写可测试代码的文章Writing Testable Code - Preston
5
为了平衡起见,PHP并不只有2个或3个单元测试框架 - 这里有一个列表:http://en.wikipedia.org/wiki/List_of_unit_testing_frameworks#PHP - Fenton
11个回答

37

还有一个名为phpt的第三方“框架”,它远比SimpleTest更易学,甚至更易学。

这里可以找到一个入门指南:http://qa.php.net/write-test.php

编辑:刚看到您要求示例代码。

假设您在一个名为lib.php的文件中有以下函数:

<?php
function foo($bar)
{
  return $bar;
}
?>

非常简单明了,你传入的参数会被返回。因此,让我们来看一下这个函数的测试,我们将调用测试文件 foo.phpt

--TEST--
foo() function - A basic test to see if it works. :)
--FILE--
<?php
include 'lib.php'; // might need to adjust path if not in the same dir
$bar = 'Hello World';
var_dump(foo($bar));
?>
--EXPECT--
string(11) "Hello World"

简而言之,我们给参数$bar赋值"Hello World",然后将调用foo()函数的响应传递给var_dump()

要运行此测试,请使用:pear run-test path/to/foo.phpt

这需要您的系统上安装PEAR,在大多数情况下这是非常普遍的。如果您需要安装它,我建议安装最新版本。如果您需要帮助设置它,请随时询问(但请提供操作系统等信息)。


应该是 run-tests 吧? - Dharman

32

有两个框架可以用于单元测试。SimpletestPHPUnit,我更喜欢后者。在PHPUnit的主页上阅读有关编写和运行测试的教程。它非常简单易懂。


22

7
我认为你提到了一篇很棒的文章。但是,你以“单元测试并不是非常有效”作为回答的开端几乎让我要点踩赞,因为我是一个测试专家...也许,用积极的措辞重新表达会鼓励人们去阅读这篇文章。 - xtofl
2
@xtofl编辑了它,稍微提高了“积极性” :) - icc97

13

我自己动手实现了这个功能,因为我没有时间学习别人的做法,撰写这篇文章大约用了20分钟,并花费10分钟进行适应以发布在此处。

对我来说,单元测试非常有用。

这篇文章有点长,但它解释得很清楚,在底部还有一个示例。

/**
 * Provides Assertions
 **/
class Assert
{
    public static function AreEqual( $a, $b )
    {
        if ( $a != $b )
        {
            throw new Exception( 'Subjects are not equal.' );
        }
    }
}

/**
 * Provides a loggable entity with information on a test and how it executed
 **/
class TestResult
{
    protected $_testableInstance = null;

    protected $_isSuccess = false;
    public function getSuccess()
    {
        return $this->_isSuccess;
    }

    protected $_output = '';
    public function getOutput()
    {
        return $_output;
    }
    public function setOutput( $value )
    {
        $_output = $value;
    }

    protected $_test = null;
    public function getTest()
    {
        return $this->_test;
    }

    public function getName()
    {
        return $this->_test->getName();
    }
    public function getComment()
    {
        return $this->ParseComment( $this->_test->getDocComment() );
    }

    private function ParseComment( $comment )
    {
        $lines = explode( "\n", $comment );
        for( $i = 0; $i < count( $lines ); $i ++ )
        {
            $lines[$i] = trim( $lines[ $i ] );
        }
        return implode( "\n", $lines );
    }

    protected $_exception = null;
    public function getException()
    {
        return $this->_exception;
    }

    static public function CreateFailure( Testable $object, ReflectionMethod $test, Exception $exception )
    {
        $result = new self();
        $result->_isSuccess = false;
        $result->testableInstance = $object;
        $result->_test = $test;
        $result->_exception = $exception;

        return $result;
    }
    static public function CreateSuccess( Testable $object, ReflectionMethod $test )
    {
        $result = new self();
        $result->_isSuccess = true;
        $result->testableInstance = $object;
        $result->_test = $test;

        return $result;
    }
}

/**
 * Provides a base class to derive tests from
 **/
abstract class Testable
{
    protected $test_log = array();

    /**
     * Logs the result of a test. keeps track of results for later inspection, Overridable to log elsewhere.
     **/
    protected function Log( TestResult $result )
    {
        $this->test_log[] = $result;

        printf( "Test: %s was a %s %s\n"
            ,$result->getName()
            ,$result->getSuccess() ? 'success' : 'failure'
            ,$result->getSuccess() ? '' : sprintf( "\n%s (lines:%d-%d; file:%s)"
                ,$result->getComment()
                ,$result->getTest()->getStartLine()
                ,$result->getTest()->getEndLine()
                ,$result->getTest()->getFileName()
                )
            );

    }
    final public function RunTests()
    {
        $class = new ReflectionClass( $this );
        foreach( $class->GetMethods() as $method )
        {
            $methodname = $method->getName();
            if ( strlen( $methodname ) > 4 && substr( $methodname, 0, 4 ) == 'Test' )
            {
                ob_start();
                try
                {
                    $this->$methodname();
                    $result = TestResult::CreateSuccess( $this, $method );
                }
                catch( Exception $ex )
                {
                    $result = TestResult::CreateFailure( $this, $method, $ex );
                }
                $output = ob_get_clean();
                $result->setOutput( $output );
                $this->Log( $result );
            }
        }
    }
}

/**
 * a simple Test suite with two tests
 **/
class MyTest extends Testable
{
    /**
     * This test is designed to fail
     **/
    public function TestOne()
    {
        Assert::AreEqual( 1, 2 );
    }

    /**
     * This test is designed to succeed
     **/
    public function TestTwo()
    {
        Assert::AreEqual( 1, 1 );
    }
}

// this is how to use it.
$test = new MyTest();
$test->RunTests();

这将输出:

测试:TestOne 失败 
/**
* 这个测试旨在失败
**/(行:149-152;文件:/Users/kris/Desktop/Testable.php)
测试:TestTwo 成功

7

获取PHPUnit。它非常易于使用。

然后从非常简单的断言开始。在进入其他内容之前,您可以使用AssertEquals做很多事情。这是一个让你入门的好方法。

您可能还想尝试先编写测试(因为您给出了TDD标签),然后再编写代码。如果您以前没有这样做过,那么这将是一种启示。

require_once 'ClassYouWantToTest';
require_once 'PHPUnit...blah,blah,whatever';

class ClassYouWantToTest extends PHPUnit...blah,blah,whatever
{
    private $ClassYouWantToTest;

   protected function setUp ()
    {
        parent::setUp();
        $this->ClassYouWantToTest = new ClassYouWantToTest(/* parameters */);
    }

    protected function tearDown ()
    {
        $this->ClassYouWantToTest = null;
        parent::tearDown();
    }

    public function __construct ()
    {   
        // not really needed
    }

    /**
     * Tests ClassYouWantToTest->methodFoo()
     */
    public function testMethodFoo ()
    {
        $this->assertEquals(
            $this->ClassYouWantToTest->methodFoo('putValueOfParamHere), 'expectedOutputHere);

    /**
     * Tests ClassYouWantToTest->methodBar()
     */
    public function testMethodFoo ()
    {
        $this->assertEquals(
            $this->ClassYouWantToTest->methodBar('putValueOfParamHere), 'expectedOutputHere);
}

5

对于简单的测试和文档,php-doctest 是相当不错的选择,它非常易于入门,因为您无需打开单独的文件。想象一下下面的函数:

/**
* Sums 2 numbers
* <code>
* //doctest: add
* echo add(5,2);
* //expects:
* 7
* </code>
*/
function add($a,$b){
    return $a + $b;   
}

如果您现在通过phpdt(php-doctest的命令行运行程序)运行此文件,将运行1个测试。 Doctest包含在代码块中。Doctest起源于Python,可以为代码如何工作提供有用且可运行的示例。您不能仅使用它,因为代码本身会混杂着测试用例,但我发现它与更正式的TDD库一起使用很有用-我使用phpunit。
这里的第一个答案在这里很好地总结了它(这不是单元测试与doctest之间的对比)。

1
这不会让源代码有点混乱吗? - Ali Ghanavatian
它可以用于单个简单测试,但应仅用作文档。如果您需要更多,请使用单元测试。 - scc

2

Codeception测试与常规单元测试类似,但在需要模拟和存根的情况下更加强大。

以下是控制器测试的示例。请注意存根的创建非常容易,您可以轻松检查方法是否被调用。

<?php
use Codeception\Util\Stub as Stub;

const VALID_USER_ID = 1;
const INVALID_USER_ID = 0;

class UserControllerCest {
public $class = 'UserController';


public function show(CodeGuy $I) {
    // prepare environment
    $I->haveFakeClass($controller = Stub::makeEmptyExcept($this->class, 'show'));
    $I->haveFakeClass($db = Stub::make('DbConnector', array('find' => function($id) { return $id == VALID_USER_ID ? new User() : null ))); };
    $I->setProperty($controller, 'db', $db);

    $I->executeTestedMethodOn($controller, VALID_USER_ID)
        ->seeResultEquals(true)
        ->seeMethodInvoked($controller, 'render');

    $I->expect('it will render 404 page for non existent user')
        ->executeTestedMethodOn($controller, INVALID_USER_ID)
        ->seeResultNotEquals(true)
        ->seeMethodInvoked($controller, 'render404','User not found')
        ->seeMethodNotInvoked($controller, 'render');
}
}

此外还有其他很酷的东西。您可以测试数据库状态、文件系统等。


2

PHPUnit是PHP的事实标准单元测试框架。还有一些其他框架,例如DocTest(作为PEAR包可用)。 PHP本身通过phpt测试进行回归测试等方面的测试,这些测试也可以通过PEAR运行。


1
除了已经提供的关于测试框架的优秀建议之外,您是否正在使用其中一个具有自动化测试功能的PHP Web框架来构建您的应用程序,例如SymfonyCakePHP? 有时,只需要将测试方法放到指定位置就可以降低某些人与自动化测试和TDD相关的启动摩擦。

1
Way too much to re-post here, but here is a great article on using phpt. It covers a number of aspects around phpt that are often overlooked, so it could be worth a read to expand your knowledge of php beyond just writing a test. Fortunately the article also discusses writing tests!

讨论的主要观点:

  1. 了解 PHP 的较少文档化方面(或者几乎任何部分)的工作原理
  2. 为自己的 PHP 代码编写简单的单元测试
  3. 编写测试作为扩展的一部分,或将潜在的错误传达给内部或 QA 组

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接