Блог/Разработка с PHPUnit

TDD разработка с использованием PHPUnit

Автор: Кудашев Сергей

Долго доходил до TDD (Test-Driven Development) и таки добрался. Начав применять его в разработке решил немного написать про него, взяв за основу модульную разработку на PHP с использованием фреймворка PHPUnit при разработке. Давайте углубимся в него на примере простого приложения.

Разрабатывать я буду простой класс URL кодирования/декодирования, результат работы которого будет доступен в моем наборе инструментов в виде небольшого сервиса кодирования/декодирования URL. Иногда для разных тестов мне необходимы кодированные строки, поэтому решил по быстрому набросать этот класс, и иметь данный арсенал под рукой. Вообще приятно, когда под рукой имеешь собственные утилиты, которые можешь доработать под себя в любой момент.

Как мы знаем, не все данные могут передаваться в URL и правильно обрабатываться веб-серверов. Если говорить упрощенно, согласно рекомендаций RFC1738 (URL) и RFC3986 (URI) в URL должен быть составлен из ограниченного набора ASCII символов, который включает в себя цифры, буквы и некоторые специальные символы. Остальные символы, не входящие в этот набор, должны преобразовываться специальным образом для их правильной обработки веб сервером. И хотя четкого требования к обязательному кодированию нет, желательно его придерживаться (а в случае с пробелом, обязательно :) ).

Итак, получив немного теоретических знаний обратимся к PHP. В данном языке реализовано две группы функций кодирования/декодирования URL (urlencode/urldecode и rawurlencode/rawurldecode). Разница между двумя группами заключается в том, что первая группа преобразует пробел в знак +, а вторая группа преобразует пробелы в символы %20, что более соответствует RFC 3986. Теперь мы точно знаем, что и как будем тестировать.

Не буду подробно касаться момента установки PHPUnit, только широкими мазками. Устанавливаем composer, создаем в выбранной нами папке файл composer.json со следующим содержимым:

{
	"require": {
		"phpunit/phpunit": "5.*"
	}
}

Все, делаем composer install, и после установки пакетов подгружаем его в проект, я использую autoloader из vendor\autoload.php. После этого я создаю папку с тестами, куда записываются все тесты, связанные с проектом и оттуда же выполняются, это удобно, когда хочется каждый раз выполнять все связанные с проектом тесты.

Итак, суть TDD состоит примерно в том, что перед тем, как написать код, нам надо сначала написать тесты. Зная, что у мы будем использовать два варианта кодирования подготовим ожидаемые значения:

Th	e te$t tExt! => Th%09e+te%24t+tExt%21
Th	e te$t tExt! => Th%09e%20te%24t%20tExt%21

Далее мы пишем класс, можно скачать по ссылке, в котором реализуем кодирование/раскодирование поступивших данных через метод getDecoder.

<?php

class MyDecoder
{

	protected $properties = [];

	public function __construct()
	{
		$this->properties['code'] = isset($_POST['code']) ? 'decode' : 'encode';
		$this->properties['type'] = isset($_POST['type']) ? 'rawurl' : 'url';
	}


	public function __destruct()
	{
	}

	/**
	* @param $in input from anywhere
	* @return string
	*/
	public function getDecoder($in = null)
	{
		return $this->decoder($in);
	}

	protected function decoder($in = null)
	{
		$func = $this->properties['type'].$this->properties['code'];

		$out = $func($in);

		return $out;
	}

	public function getProperties($exact = null)
	{
		if (array_key_exists($exact, $this->properties)) {
			return $this->properties[$exact];
		} else {
			return $this->properties;
		}
	}

	public function setProperties($exact, $val = true)
	{
		if (array_key_exists($exact, $this->properties)) {
			$this->properties[$exact] = $val;
			return true;
		} else {
			return false;
		}
	}

}

Несколько моментов, которые хотелось бы отразить по данному классу:

- выбор метода кодирования у нас задается в конструкторе, через присвоение значений ключам code и type во внутреннем массиве настроек. Так как в нашем случае этот код не привязан ни к какому веб интерфейсу, то задавать данные опции мы будем через специальный метод setProperties(), который так же протестируем.

- кодирование/декодирование осуществляется через приватный метод decoder(), обращаться к которому мы будем через специально созданный для этого публичный метод getDecoder(). Для интересующихся, это сделано специально, так как дает большую гибкость в возможности использования основного метода через разные интерфейсные методы, например, можно написать отдельный публичный метод, который будет обрабатывать на входе не строку, а массив строк.

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

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

require_once __DIR__ . '/decoder.class.php';

class DecoderTest extends PHPUnit_Framework_TestCase
{ 
	protected $testClass;

	public function setUp()
	{
		parent::setUp();
		$this->testClass = new MyDecoder();

	}

	public function tearDown()
	{
		parent::tearDown();
		$this->testClass = null;
	}

	public function testProperties() {

		$this->assertEquals(false, $this->testClass->setProperties('noexist', true));
		$this->assertEquals(true, $this->testClass->setProperties('code', 'decode'));
		$this->assertEquals('decode', $this->testClass->getProperties('code'));
		$this->assertEquals(true, $this->testClass->setProperties('type', 'rawurl'));
		$this->assertEquals('rawurl', $this->testClass->getProperties('type'));

	}

	public function testDecoder() {

		$this->assertEquals(true, $this->testClass->setProperties('code', 'encode'));
		$this->assertEquals(true, $this->testClass->setProperties('type', 'url'));
		$this->assertEquals('Th%09e+te%24t+tExt%21', $this->testClass->getDecoder('Th e te$t tExt!'));

		$this->assertEquals(true, $this->testClass->setProperties('code', 'encode'));
		$this->assertEquals(true, $this->testClass->setProperties('type', 'rawurl'));
		$this->assertEquals('Th%09e%20te%24t%20tExt%21', $this->testClass->getDecoder('Th e te$t tExt!'));

		$this->assertEquals(true, $this->testClass->setProperties('code', 'decode'));
		$this->assertEquals(true, $this->testClass->setProperties('type', 'url'));
		$this->assertEquals('Th e te$t tExt!', $this->testClass->getDecoder('Th%09e+te%24t+tExt%21'));

		$this->assertEquals(true, $this->testClass->setProperties('code', 'decode'));
		$this->assertEquals(true, $this->testClass->setProperties('type', 'rawurl'));
		$this->assertEquals('Th e te$t tExt!', $this->testClass->getDecoder('Th%09e%20te%24t%20tExt%21'));

	}

}

Имя класса должно содержать имя тестируемого класса. Переменная $ testClass будет содержать экземпляр тестового класса, к которому мы будем обращаться. Методы setUp() и tearDown() работают по аналоги с конструктором и деструктором. В первом мы создаем экземпляр тестового класса, в последней мы его обнуляем (это не обязательно, но я делаю это по привычке).

Далее идут два публичных метода, которые собственно и отвечают за тестировании. Это testProperties( отвечает за работу с настройками класса ) и testDecoder( отвечает за работу кодирования/декодирования ). Методы, включающие тесты должны начинаться на test, иначе они не будут выполняться. Все представленные тесты построены только на нескольких самых простых методах PHPUnit, а нам больше и не надо (полный перечень методов можно найти в документации https://phpunit.de/manual/current/en/appendixes.assertions.html#appendixes.assertions.assertStringStartsWith).

Большинство методов PHPUnit работают на проверку значений, получаемых от выполнения определенного кода. Причем эти значения как могут быть уже определены самим методом (assertTrue(mixed $actual) ожидает получить true от кода), так и мы можем определять данные ожидаемые значения, что дает нам невероятную гибкость. Таким методом является метод assertEquals(mixed $expected, mixed $actual), который сравнивает ожидаемое значение ($expected), которое мы задаем сами, со значением реально полученным, в нашем случае от объекта ($actual). Если значения ожидаемого и реального совпадут, то тест пройдет, а если не совпадут, то тест вывалится с ошибкой, указав, что именно в этом месте возникла проблема.

И вот тут-то нам и пригодятся те ожидаемые значения, которые мы определили себе заранее. Вот часть проверки:

$this->assertEquals(true, $this->testClass->setProperties('code', 'encode'));
$this->assertEquals(true, $this->testClass->setProperties('type', 'url'));
$this->assertEquals('Th%09e+te%24t+tExt%21', $this->testClass->getDecoder('Th e te$t tExt!'));

Первыми двумя строчками мы задаем каким образом будет обрабатываться строка (в нашем случае это будет простое кодирование функцией urlencode), параллельно проверяя, установились ли данные значения, так как мы ожидаем получить true от выполнения метода setProperties(). В третьей строчек происходит та самая магия, ради которой это все писалось. Мы передаем в метода getDecode необработанные данные и ожидаем получить обработанную определенным образом строку. Так как наш класс написан верно, то тесты отрабатываются верно.

PHPUnit tests good

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

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

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

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

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

Комментарии (0)