Йоптыть, как же мы будем это тестировать?

Михаил Боднарчук @davert

О чем этот доклад

Кто это такой

$my->name    = 'Михаил Боднарчук';
$my->twitter = '@davert';
$my->city    = 'Киев';

$my->projects[] = 'Codeception';
$my->projects[] = 'AspectMock';
$my->projects[] = 'JSter.net';

$my->hobbies = 'tourism';

Тестировать или нет?

Когда не надо тестировать

Зачем мы пишем тесты

Какой бы мы фреймворк не выбрали, качественное приложение без эффективных тестов нам не написать.

Виды тестов

Критерии тестов

Что выбрать для тестирования?

Юнит тестирования

PHPUnit

мастодонт юнит тестирования

phpunit.de

Плюсы

Пример

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

Минусы

Аццкий код

// PHPUnit_Framework_TestCase
public function getMock($originalClassName, ...)
{            
  $mockObject = PHPUnit_Framework_MockObject_Generator::getMock(
    $originalClassName,
    $methods,
    $arguments,
    $mockClassName,
    $callOriginalConstructor,
    $callOriginalClone,
    $callAutoload,
    $cloneArguments
  );
  

atoum

идейный наследник PHPUnit

Особенности

Пример

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

Минусы

PhpSpec

SpecBDD: Разработка через тестирование.

Особенности

Пример

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

Минусы

Для любознательных: 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

Specify + Verify

BDD for PHPUnit

github.com/codeception/specifygithub.com/codeception/verify

Особенности

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?

Фреймворки для моков

Mockery vs PHPUnit

$phpunitMock = $this->getMock('AClassToBeMocked');
$phpunitMock->expects($this->exactly(2))->method('someMethod');

$mockeryMock = \Mockery::mock('AnInexistentClass');
$mockeryMock->shouldReceive('someMethod')->twice();

Минусы

Нетестируемый код

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

Минусы

Функциональное тестирование

Codeception

Всё включено

Особенности

Пример

$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']);          

Минусы

Выводы

Спасибо за внимание

Михаил Боднарчук @davert

Ссылки: