Юнит тестирование с помощью PHPUnit. Аннотации и mock-объекты.

14-04-25 Php, Разное PHPUnit, Tests 0

В предыдущих статьях (Основы Unit тестирования в PHP с помощью PHPUnit и Вникаем в assert-методы) мы рассмотрели основы тестирования приложений на PHP. Юнит тесты сильно упрощают процесс разработки. Благодаря автоматизированным тестам вы можете изменять любой участок кода вашего приложения и быть уверенным, что ничего не сломается. В этой статье мы рассмотрим еще пару возможностей PHPUnit — работу с аннотациями и mock-объекты.

Аннотации

Если вы читали книги и оставляли заметки, то вы знакомы с понятием аннотации. В некотором роде, даже комментарий, который вы написали к своему тесту, можно воспринимать как аннотацию. Например:


<?php

class MyTestClass extends PHPUnit_Framework_TestCase {

    /**
     * Testing the answer to “do you love unit tests?”
     */
    public function testDoYouLoveUnitTests() {

        $love = true;

        $this->assertTrue($love);
    }

}

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

В общем смысле — это те же самые комментарии, и ни чем не отличаются от обычных, заключенных в /* */ или //. Но если использовать их вместе с PHPUnit, можно изменить поведение программы и выполнять некоторые действия автоматически.

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

В PHPUnit есть механизм, который парсит комментарии и ищет определенные фразы для выполнения определенных действий. Начнем с очень полезной фичи — аннотация, которая помогает при генерации тестов. Напишем класс для тестирования:

<?php

class MyMathClass {

    /**
     * Сумма двух чисел
     */
    public function addValues($a, $b) {
        return $a + $b;
    }

}

Теперь необходимо убедиться, что законы математики не будут нарушены. Для этого надо написать тесты. Конечно для этого примера написать тест очень легко. Но лень. Почему бы не поручить это дело PHPUint?

Для этого можно использовать Skeleton Generator, указав имя класса, который мы хотим тестировать и все будет сделано за нас.

Так как  это часть функционала PHPUnit мы будем использовать команду phpunit, но с определенным параметром. Сохраните ваш класс в файл MyMathClass.php. Для использования Skeleton Generator’а, используйте команду:

./phpunit –skeleton-test MyMathClass

Тестирующий класс будет записан в файл MyMathClassTest.php:

<?php

require_once '/path/to/MyMathClass.php';

/**
 * Test class for MyMathClass.
 * Generated by PHPUnit on 2011-02-07 at 12:22:07.
 */
class MyMathClassTest extends PHPUnit_Framework_TestCase {

    /**
     * @var MyMathClass
     */
    protected $object;

    /**
     * Sets up the fixture, for example, opens a network connection.
     * This method is called before a test is executed.
     */
    protected function setUp() {
        $this->object = new MyMathClass;
    }

    /**
     * Tears down the fixture, for example, closes a network connection.
     * This method is called after a test is executed.
     */
    protected function tearDown() {
    }

    /**
     * @todo Implement testAddValues().
     */
    public function testAddValues() {
        // Remove the following lines when you implement this test.

        $this->markTestIncomplete(
                'This test has not been implemented yet.'
        );
    }
}

Как видите все необходимое для реализации тестирующих методов было сгенерировано автоматически.

Генератор обнаружил метод addValues, но он не знает, что этот метод делает и оставил его реализацию пустой и пометил его как незавершенный.

С помощью аннотации @assert можно сообщить фреймворку что должна делать тестируемая функция. Взглянем на метод addValues еще раз:

class MyMathClass {

    /**
     * Сумма двух чисел
     * @assert (1,2) == 3
     */
    public function addValues($a, $b) {
        return $a + $b;
    }

}

Если мы запустим команду phpunit –skeleton-test снова, вы заметите изменения в созданном файле:

/**
 * Generated from @assert (1,2) == 3.
 */
public function testAddValues() {
    $this->assertEquals(
            3, $this->object->addValues(1, 2)
    );
}

PHPUnit сгенерировал тест для нашего метода автоматически!

Это произошло, потому что мы указали фреймворку, чего мы хотим от нашего метода с помощью аннотации. Вы можете использовать аннотацию @assert с большинством логических операций: больше, меньше, равно и т.д.

Можно использовать несколько таких аннотаций для одного теста.

Еще одна полезная аннотация — @dataProvider —  простой способ передачи стандартных наборов данных в тест.

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

<?php
/**
 * Data provider for test methods below
 */
public static function provider() {
    return array(
        array(1, 2, 3),
        array(4, 2, 6),
        array(1, 5, 7)
    );
}

/**
 * Testing addValues returns sum of two values
 * @dataProvider provider
 */
public function testAddValues($a, $b, $sum) {
    $this->assertEquals(
            $sum, $this->object->addValues($a, $b)
    );
}

Мы добавили метод provider и аннотацию @dataProvider к тестируемому методу.

Для каждого значения массива, который возвращает метод provider, будет выполнен тестируемый метод. Таким образом мы получаем возможность протестировать разные наборы входных данных в рамках одного теста. Значения, возвращаемых массивов будут переданы в тестируемую функцию соответственно их позициям: значение с индексом 0 будет передано в параметре $a, с индексом 1 — $b и т.д.

Мы рассмотрели только две аннотации, конечно,  PHPUnit поддерживает и другие (полный список можно посмотреть в документации), среди которых:

  • Аннотация @expectedException, которая ожидает исключение. Если исключение не будет возбуждено, тест провалится.
  • Аннотация @depends для обозначения связи между вашими тестами. Например, вам необходимо, чтобы некоторый тест выполнялся только если другой был успешным, если тест не был успешным, зависимый тест будет пропущен.

Mock-объекты

Иногда при тестировании необходимо использовать объекты, поведение которых мы не можем контролировать. Для решения этой проблемы можно использовать mock-объекты (в переводе с английского — пародия).

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

Выход — создание mock-объекта, который будет имитировать нужное нам состояние объекта. Наш класс для связи с бд:

<?php

class Database {
    /**
     * Этот запрос всегда выполняется очень долго
     */
    public function reallyLongTime() {
        // результат запроса будет возвращаться в виде массива $results
        $results = array(
            array(1, 'test', 'foo value')
        );

        sleep(100);

        return $results;
    }
}

Это базовый класс, в котором объявлен долговыполняющийся запрос. В методе вызывается функция sleep, таким образом мы сможем понять работаем с mock-объектом, вместо реального.

<?php

require_once '/path/to/Database.php';

class DatabaseTest extends PHPUnit_Framework_TestCase {

    private $db = null;

    public function setUp() {
        $this->db = new Database();
    }

    public function tearDown() {
        unset($this->db);
    }

    /**
     * Test that the "really long query" always returns values
     */
    public function testReallyLongReturn() {
        $mock = $this->getMock('Database');
        $result = array(
            array(1, 'foo', 'bar test')
        );

        $mock->expects($this->any())
                ->method('reallyLongTime')
                ->will($this->returnValue($result));

        $return = $mock->reallyLongTime();

        $this->assertGreaterThan(0, count($return));
    }
}

Метод getMock создает mock-объект с таким же набором методов, как и обычный объект класса Database. Все методы, по-умолчанию, будут возвращать null, но мы можем перегрузить нужные нам методы. В нашем примере мы перегружаем метод reallyLongTime для изменения его поведения под наши нужды:

    $mock->expects($this->any())
            ->method('reallyLongTime')
            ->will($this->returnValue($result));

Эти три линии означают, что при каждом вызове метода reallyLongTime объекта $mock будет возвращаться значение переменной $result, вместо реального выполнения метода. Таким образом мы избегаем мучительного ожидания завершения работы метода.

MockBuilder

MockBuilder предоставляет некоторые полезные фичи. При работе с MockBuilder первым методом в цепочке вызовов должен быть getMockBuilder(), а последним — getMock(). Например, в следующем примере мы создаем mock-объект, который не использует конструктор, а также отключает autoload:

<?php

function testReallyLongRunBuilder() {
    $stub = $this->getMockBuilder('Database')
            ->setMethods(array(
                'reallyLongTime'
            ))
            ->disableAutoload()
            ->disableOriginalConstructor()
            ->getMock();

    $result = array(array(1, 'foo', 'bar test'));

    $stub->expects($this->any())
            ->method('reallyLongTime')
            ->will($this->returnValue($result));

    $this->assertGreaterThan(0, count($return));
}

Этот пример делает то же самое, что и предыдущий, за исключением вызова двух новых методов: disableAutoload и disableOriginalConstructor. По их названию понятно, что они делают.

Еще одна полезная возможность mock объектов — проверка аргументов переданных при вызове функции. Для этого используется метод with(). В нашем примере мы используем этот метод для проверки типа переданного аргумента:

<?php

/**
 * Проверка типа входных данных
 */
public function ttestReallyLongRunBuilderConstraint() {
    $stub = $this->getMock('Database', array('reallyLongTime'));

    $stub->expects($this->any())
            ->method('reallyLongTime')
            ->with($this->isType('array'));

    $arr = array('test');

    $this->assertTrue($stub->reallyLongRun($arr));
}

Методу with() можно передать любое количество аргументов, соответствующее тестируемому методу. В качестве параметра метод with() может принимать конструкции: 

  • arrayHasKey()
  • contains()
  • equalTo()
  • attributeEqualTo()
  • fileExists()
  • greaterThan()
  • isFalse()
  • isInstanceOf()
  • isNull()
  • lessThan()
  • lessThanOrEqual()
  • matchesRegularExpression()
  • stringContains()

и д.р.

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

На этом всё! 🙂

Хочешь получать статьи на почту?

Подпишись на обновления!
* Ваш email не будет разглашен/продан. Вы сможете отписаться в любое время.

Нет комментариев

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *