Как писать модульные тесты на PHP? [закрыто]


98

Я везде читал о том, насколько они хороши, но по какой-то причине я не могу понять, как именно я должен что-то тестировать. Может ли кто-нибудь опубликовать пример кода и то, как они его протестируют? Если не беда :)


5
Для баланса не существует 2 или 3 фреймворков для модульного тестирования PHP - здесь есть список: en.wikipedia.org/wiki/List_of_unit_testing_frameworks#PHP
Fenton

Ответы:


36

Существует третий «фреймворк», который намного легче освоить - даже проще, чем простой тест, он называется phpt.

Праймер можно найти здесь: 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"и var_dump()ответ на вызов функции foo().

Чтобы запустить этот тест, используйте: pear run-test path/to/foo.phpt

Для этого требуется рабочая установка PEAR в вашей системе, что довольно часто встречается в большинстве случаев. Если вам нужно установить его, я рекомендую установить последнюю доступную версию. Если вам нужна помощь в настройке, не стесняйтесь спрашивать (но укажите ОС и т. Д.).


Не должно быть run-tests?
Дхарман

31

Для модульного тестирования можно использовать две среды. Simpletest и PHPUnit , что я предпочитаю. Прочтите учебники о том, как писать и запускать тесты на домашней странице PHPUnit. Это довольно просто и хорошо описано.


22

Вы можете сделать модульное тестирование более эффективным, изменив свой стиль кодирования в соответствии с ним.

Я рекомендую просмотреть блог Google Testing Blog , в частности, сообщение о написании тестируемого кода .


7
Я думаю, вы упомянули отличный пост. Начав свой ответ с «Модульное тестирование не очень эффективно», я чуть не проголосовал против, хотя, будучи опытным тестировщиком ... Возможно, перефразирование в положительной манере побудило бы людей прочитать статью.
xtofl 05

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 тест. Доктест содержится внутри блока <code>. Doctest возник на Python и отлично подходит для предоставления полезных и работоспособных примеров того, как должен работать код. Вы не можете использовать его исключительно, потому что сам код будет засорять тестовыми примерами, но я обнаружил, что он полезен вместе с более формальной библиотекой tdd - я использую phpunit.

Этот первый ответ здесь хорошо подводит итог (это не unit vs doctest).


1
разве это не мешает источнику?
Али Ганаватян

это может. его следует использовать только для одиночных простых тестов. также служит документацией. если вам нужно больше, используйте unit test.
София

2

phpunit - это, по сути, фреймворк для модульного тестирования php. есть также DocTest (доступный в виде пакета PEAR) и несколько других. Сам php тестируется на регрессии и тому подобное с помощью тестов phpt, которые также можно запускать через грушу.


2

Тесты на кодирование во многом похожи на обычные модульные тесты, но очень эффективны в тех случаях, когда вам нужны имитации и заглушки.

Вот пример теста контроллера. Обратите внимание, как легко создаются заглушки. Как легко вы проверяете, что метод был вызван.

<?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');
}
}

Также есть другие крутые штуки. Вы можете проверить состояние базы данных, файловую систему и т. Д.


1

Помимо уже приведенных отличных предложений по фреймворкам тестирования, создаете ли вы свое приложение с помощью одной из веб-фреймворков PHP, в которую встроено автоматическое тестирование, например, Symfony или CakePHP ? Иногда наличие места, где можно просто добавить свои методы тестирования, снижает трение при запуске, которое некоторые люди связывают с автоматическим тестированием и TDD.


1

Слишком много, чтобы повторно публиковать здесь, но вот отличная статья об использовании phpt . Он охватывает ряд аспектов phpt , которые часто упускаются из виду, поэтому, возможно, стоит прочитать его, чтобы расширить свои знания о php, помимо написания теста. К счастью, в статье также обсуждается написание тестов!

Основные темы обсуждения

  1. Узнайте, как работают мало задокументированные аспекты PHP (или почти любая часть в этом отношении)
  2. Напишите простые модульные тесты для вашего собственного PHP-кода
  3. Пишите тесты как часть расширения или чтобы сообщить о потенциальной ошибке внутренним организациям или группам QA

1

Я знаю, что здесь уже есть много информации, но, поскольку она все еще отображается в поиске Google, я мог бы добавить в список Chinook Test Suite . Это простой и небольшой фреймворк для тестирования.

С его помощью вы можете легко тестировать свои классы, а также создавать имитационные объекты. Вы запускаете тесты через веб-браузер и (пока еще нет) через консоль. В браузере вы можете указать, какой тестовый класс или даже какой тестовый метод запускать. Или вы можете просто запустить все тесты.

Скриншот со страницы github:

Фреймворк модульного тестирования Chinook

Что мне нравится в этом, так это то, как вы утверждаете тесты. Это делается с помощью так называемых «беглых утверждений». Пример:

$this->Assert($datetime)->Should()->BeAfter($someDatetime);

И создание фиктивных объектов тоже очень просто (с плавным синтаксисом):

$mock = new CFMock::Create(new DummyClass());
$mock->ACallTo('SomeMethod')->Returns('some value');

В любом случае, дополнительную информацию можно найти на странице github с примером кода:

https://github.com/w00/Chinook-TestSuite

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.