PHPUnit - Глава 12. Test-Driven Development

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

Вы можете использовать PHPUnit после того как программа написана. Онднако, чем раньше вы напишите тест после внесения ошибки тем больше пользы этот тест принесёт. Поэтому, вместо того чтобы писать тесты месяцы спустя после того как код "закончен", мы можем писать тесты спустя дни или часы или даже минуты после того как был внесён дефект. Но зачем останавливаться на этом? Почему бы не писать тесты немного раньше возможного внесения дефекта?

Тестирование до программирования, которое является частью экстремального программирования и разработки управляемой тестами (TDD - Test-Driven-Development), построено на этой идее, которая возведенной в абсолют. При сегодняшней вычислительной мощности у нас есть возможность запускать тысячи тестов тысячи раз в день. Мы можем использовать обратную связь полученную от этих тестов для программирования маленькими шагами, каждый из которых несёт с собой уверенность нового автотеста в доплнение ко всем тестам написаным ранее. Тесты как страховочные крючья - они не дадут вам упасть ниже того места до котрого уже продвинулись.

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

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

Dan North

Далее следует краткое введение в разработку через тестирование. Вы можете изучить эту тему более подробно в книгах Test-Driven Development [Beck2002] Кента Бека (Kent Beck) и Дейва Астелса (Dave Astels) A Practical Guide to Test-Driven Development [Astels2003].

 Пример: банковский счёт (BankAccount)

В этом разделе мы рассмотрим пример класса, который представляет банковский счёт. Контракт для BankAccount требует не только методы для установи и получения баланса банковского счёта, но и методы для пополнения и вывода денег. Кроме этого он устанавливает следующие два условия, которые должны соблюдаться:

  • Начальный баланс счёта должен быть равен нулю.

  • Баланс не может стать отрицательным.

Мы пишем тесты для класса BankAccount до того как писать код самого класса. Мы используем условия контракта как основу для тестов и именуем тестовые методы соответственно, как показано в Пример 12.1, «Тесты для класса BankAccount».

Пример 12.1. Тесты для класса BankAccount

<?php
require_once 'BankAccount.php';

class BankAccountTest extends PHPUnit_Framework_TestCase
{
    protected $ba;

    protected function setUp()
    {
        $this->ba = new BankAccount;
    }

    public function testBalanceIsInitiallyZero()
    {
        $this->assertEquals(0, $this->ba->getBalance());
    }

    public function testBalanceCannotBecomeNegative()
    {
        try {
            $this->ba->withdrawMoney(1);
        }

        catch (BankAccountException $e) {
            $this->assertEquals(0, $this->ba->getBalance());

            return;
        }

        $this->fail();
    }

    public function testBalanceCannotBecomeNegative2()
    {
        try {
            $this->ba->depositMoney(-1);
        }

        catch (BankAccountException $e) {
            $this->assertEquals(0, $this->ba->getBalance());

            return;
        }

        $this->fail();
    }
}
?>

Теперь мы пишем минимально необходимое количество кода, для того чтобы первый тест testBalanceIsInitiallyZero(), прошёл. В нашем примере это имплементация метода getBalance() класса BankAccount как показано Пример 12.2, «Код необходимый для того, чтобы тест testBalanceIsInitiallyZero() прошёл». 

Пример 12.2. Код необходимый для того, чтобы тест testBalanceIsInitiallyZero() прошёл

<?php
class BankAccount
{
    protected $balance = 0;

    public function getBalance()
    {
        return $this->balance;
    }
}
?>

Тест для первого условия контракта сейчас проходит, но тесты для второго условия контракта падают, так как мы еще не реализовали методы которые вызывают эти тесты.

phpunit BankAccountTest
PHPUnit 3.7.0 by Sebastian Bergmann.

.
Fatal error: Call to undefined method BankAccount::withdrawMoney()

Для тестов, которые обеспечивают второе условие контракта, мы должны реализовать методы withdrawMoney(), depositMoney(), и setBalance() как показано в Пример 12.3, «Законченый класс BankAccount». Эти методы написаны таким образом чтобы вызывать исключение BankAccountException когда они вызваны с неверными значениями, которые могут нарушить условия контракта.

Пример 12.3. Законченый класс BankAccount

<?php
class BankAccount
{
    protected $balance = 0;

    public function getBalance()
    {
        return $this->balance;
    }

    protected function setBalance($balance)
    {
        if ($balance >= 0) {
            $this->balance = $balance;
        } else {
            throw new BankAccountException;
        }
    }

    public function depositMoney($balance)
    {
        $this->setBalance($this->getBalance() + $balance);

        return $this->getBalance();
    }

    public function withdrawMoney($balance)
    {
        $this->setBalance($this->getBalance() - $balance);

        return $this->getBalance();
    }
}
?>

Теперь тесты, которые проверяют второе условие контракта тоже проходят:

phpunit BankAccountTest
PHPUnit 3.7.0 by Sebastian Bergmann.

...

Time: 0 seconds


OK (3 tests, 3 assertions)

  Как альтернативу, можно использовать статические методы-утверждения класса PHPUnit_Framework_Assert для описания условий контракта в коде в виде утверждений в стиле программирования по контракту, как показано в Пример 12.4, «Класс BankAccount с утверждениями в стиле контрактного программирования». Когда какое-то из утверждений не верно будет вызвано исключение PHPUnit_Framework_AssertionFailedError. В этом случае вы пишете меньнше кода для проверки уловий контракта и тесты становятся более читабельными. Однако, вы добавляете в ваш рабочий код зависимость от PHPUnit.

Пример 12.4. Класс BankAccount с утверждениями в стиле контрактного программирования

<?php
class BankAccount
{
    private $balance = 0;

    public function getBalance()
    {
        return $this->balance;
    }

    protected function setBalance($balance)
    {
        PHPUnit_Framework_Assert::assertTrue($balance >= 0);

        $this->balance = $balance;
    }

    public function depositMoney($amount)
    {
        PHPUnit_Framework_Assert::assertTrue($amount >= 0);

        $this->setBalance($this->getBalance() + $amount);

        return $this->getBalance();
    }

    public function withdrawMoney($amount)
    {
        PHPUnit_Framework_Assert::assertTrue($amount >= 0);
        PHPUnit_Framework_Assert::assertTrue($this->balance >= $amount);

        $this->setBalance($this->getBalance() - $amount);

        return $this->getBalance();
    }
}
?>

28.03.2017 15:13