

Йоптыть, как же мы будем это тестировать?
Михаил Боднарчук @davert
О чем этот доклад
- Тестировать или нет?
- Что выбрать для тестирования?
- Каких граблей нам ожидать?
Кто это такой
$my->name = 'Михаил Боднарчук';
$my->twitter = '@davert';
$my->city = 'Киев';
$my->projects[] = 'Codeception';
$my->projects[] = 'AspectMock';
$my->projects[] = 'JSter.net';
$my->hobbies = 'tourism';
Тестировать или нет?
Когда не надо тестировать
- Проект меняется редко
- Проект сдан и всё
- Разработчиков мало, то есть я один
- Нам мало платят
- Менеджмент хочет побыстрее и некачественно.
Зачем мы пишем тесты
- Чтобы стабилизировать код
- Тесты помогают нам в проектировании (TDD, BDD)
- Тесты - лучшая документация
Какой бы мы фреймворк не выбрали, качественное приложение без эффективных тестов нам не написать.
Виды тестов

Критерии тестов
- Покрытие
- Скорость выполнения
- Поддержка
- Читабельность
Что выбрать для тестирования?
Юнит тестирования
PHPUnit
мастодонт юнит тестирования
phpunit.deПлюсы
- Стандарт де-факто
- Отчеты в формате JUnit, HTML, TAP
- Моки и Стабы
- Покрытие кода
- Зависимости
- Data Providers
- Selenium
- Listeners
Пример
public function testNoIdentityMatchCredentials() {
$password = '51d3c5593946a'; //valid pass
$username = 'some_email@example.com'; //valid email
$identity = new UserIdentity($username,$password);
$identity->authenticate();
$this->assertEquals(
UserIdentity::ERROR_NONE,
$identity->errorCode,
'user credentials must be correct');
}
Минусы
- Излишне сложный
- Конфигурация в XML
- Большинство задач требуют напильника
- Модульность (её нет)
- Аццкий код
Аццкий код
// PHPUnit_Framework_TestCase
public function getMock($originalClassName, ...)
{
$mockObject = PHPUnit_Framework_MockObject_Generator::getMock(
$originalClassName,
$methods,
$arguments,
$mockClassName,
$callOriginalConstructor,
$callOriginalClone,
$callAutoload,
$cloneArguments
);
atoum
идейный наследник PHPUnit
Особенности
- Интуитивно простой, современный (php 5.3+)
- Крутой chained DSL, читабельность тестов
- Изоляция процессов
- Уведомления
- Покрытие кода
- Cтабы и Моки
- xUnit репорты
- PHPUnit Bridge (в разработке)
Пример
public function testNoIdentityMatchCredentials() {
$password = '51d3c5593946a'; //valid pass
$username = 'some_email@example.com'; //valid email
$identity = new UserIdentity($username,$password);
$identity->authenticate();
$this->assert('user credentials must be correct')
->integer($identity->errorCode)
->isEqualTo(UserIdentity::ERROR_NONE);
}
Минусы
- Кто о нем слышал?
- А ведь более 400 звезд на GitHub
- Документация! (основная на французском)
PhpSpec
SpecBDD: Разработка через тестирование.
Особенности
- Не фреймворк для тестирования
- Проектирование через тестирование
- Для TDD/BDD
- Генерация классов через тест
- Описание связей через моки
Пример
class CartProviderSpec extends ObjectBehavior
{
/**
* @param CartStorageInterface $storage
* @param ObjectManager $manager
* @param RepositoryInterface $repository
*/
function let($storage, $manager, $repository)
{
$this->beConstructedWith($storage, $manager, $repository);
}
Пример
/**
* @param CartInterface $cart
*/
function it_looks_for_cart_in_storage_by_id($storage, $repository, $cart)
{
$storage->getCurrentCartIdentifier()->willReturn(3);
$repository->find(3)->shouldBeCalled()->willReturn($cart);
$this->getCart()->shouldReturn($cart);
}
Минусы
- Интеграционные тесты?
- Регрессионные тесты?
- Не для legacy-кода
- Нет покрытия кода
- Читабельность. Specification by example.
Для любознательных: RSpec
describe Hash do
before do
@hash = Hash.new({:hello => 'world'})
end
it "should return a blank instance" do
Hash.new.should == {}
end
it "hash the correct information in a key" do
@hash[:hello].should == 'world'
end
end
Пример
function it_looks_for_cart_in_storage_by_id($storage, $repository, $cart)
{
$storage->getCurrentCartIdentifier()->willReturn(3);
$repository->find(3)->shouldBeCalled()->willReturn($cart);
$this->getCart()->shouldReturn($cart);
}
Пример c Ruby RSpec
it 'looks for cart in storage by id' do
CartProvider.storage.current_cart_id = 3
CartProvider.cart.should == Cart.find(3)
end
Драматическая история RSpec
In 2005 I drunkenly released a dumb hack. It was called RSpec. You are victims on one of the biggest trolls ever committed. You’re welcome.
— Steven R. Baker (@srbaker) June 13, 2013
In 2005 I drunkenly released a dumb hack. It was called RSpec. You are victims on one of the biggest trolls ever committed. You’re welcome.
— Steven R. Baker (@srbaker) June 13, 2013Specify + Verify
BDD for PHPUnit
github.com/codeception/specifygithub.com/codeception/verifyОсобенности
- Читабельность тестов
- Улучшена поддержка тестов
- Изолированость
- Легкая надстройка над PHPUnit
- Exceptions
public function testAuthentication() {
$this->specify('can login with valid email and pass', function(){
$password = '51d3c5593946a';
$username = 'some_email@example.com';
$identity = new UserIdentity($username,$password);
$identity->authenticate();
verify('authenticated with no errors', $identity->errorCode)
->equals(UserIdentity::ERROR_NONE);
});
Минусы
А оно вообще нужно?
Test Doubles
Зачем нужны Test Doubles?
- Проверить работу кода в изоляции
- Отключить внешние сервисы
Фреймворки для моков
- PHPUnit
- Mockery
- Prophecy
- AspectMock
Mockery vs PHPUnit
$phpunitMock = $this->getMock('AClassToBeMocked');
$phpunitMock->expects($this->exactly(2))->method('someMethod');
$mockeryMock = \Mockery::mock('AnInexistentClass');
$mockeryMock->shouldReceive('someMethod')->twice();
Минусы
- Нельзя тестировать статические методы
- Не обойтись без Dependency Injection
- Определение моков слишком громоздкие
Нетестируемый код
class UserService {
function createUserByName($name)
$user = new User;
$user->setName($name);
$user->save();
return $user;
}
}
Громоздкий код
Mail::shouldReceive('queue')->once()
->with('emails.support', $postData, Mockery::on(function($closure) {
$message = Mockery::mock('Illuminate\Mailer\Message');
$message->shouldReceive('to')
->with('user@email.com')
->once()
->andReturn(Mockery::self());
$message->shouldReceive('subject')
->with('Support Request')
->once();
// ....
AspectMock
решает эти ограничения
$user = test::double('User', ['save' => null]));
$service = new UserService;
$service->createUserByName('davert');
$this->assertEquals('davert', $user->getName());
$user->verifyInvoked('save');
Минусы
- Конфигурация
- Магия
- Потеря скорости 15-20%
Функциональное тестирование
Codeception
Всё включено
Особенности
- Легко начать тестировать
- Функциональное тестирование от лица пользователей
- Поддержка почти всех популярных PHP фреймворков
- Тестирование API: REST, SOAP
- Основан на PHPUnit
Пример
$I = new TestGuy($scenario);
$I->wantTo('edit post');
$I->amOnPage('/posts/2/edit');
$I->see('Edit Post', 'h1');
$I->fillField('#title', 'Edited Title '.sq(1));
$I->fillField('Body:', 'And greetings for all');
$I->click('Update');
$I->see('Edited Title '.sq(1), 'h1');
Пример
$I = new ApiGuy($scenario);
$I->wantTo('get all tickets');
$I->sendGET('/tickets');
// { "ticket": {
// "title": "Bug should be fixed",
// "user": {"name": "Davert"}}
// }
$I->seeResponseIsJson();
$I->seeResponseContainsJson(['ticket' => ['title' => 'Bug should be fixed']]);
$I->seeResponseContainsJson(['name' => 'Davert']);
Минусы
- Слишком много модулей.
- Слишком много зависимостей.
- Не все фичи PHPUnit работают.
Выводы
- Выберите инструменты под себя
- Тесты надо поддерживать
- Тесты должны быть читабельными
Спасибо за внимание
Михаил Боднарчук @davert
Ссылки:
- PHPUnit phpunit.de
- atoum atoum.org
- PhpSpec phpspec.net
- Specify github.com/codeception/specify
- Mockery github.com/padraic/mockery
- AspectMock github.com/codeception/aspectmock
- Codeception codeception.com