6

Zacząłem uczyć się Zend Framework kilka lat temu po this tutorial. Tam pokazuje, że mappers są tworzone przy użyciu klasy Zend\Db\Adapter\Adapter, aby uzyskać połączenie z bazą danych i tak pracowałem z bazami danych, ponieważ bez problemów.Zend Framework 2 i PHPUnit - podszywając się pod Zend Db Adapter Adapter class

Teraz próbuję nauczyć się korzystać z PHPUnit na aplikacjach Zend i mam problemy z testowaniem funkcji w programie odwzorowującym, ponieważ nie jestem w stanie sfałszować klasy Zend\Db\Adapter\Adapter.

This tutorial na stronie Zend pokazuje kpiny z połączeń z bazami danych, ale używa klasy Zend\Db\TableGateway\TableGateway. Wszystkie inne samouczki znalazłem w Internecie użyć tej klasy także, a jedyną rzeczą, jaką znalazłem w odniesieniu do klasy Zend\Db\Adapter\Adapter jest this:

$date = new DateTime(); 
$mockStatement = $this->getMock('Zend\Db\Adapter\Driver\Pdo\Statement'); 
$mockStatement->expects($this->once())->method('execute')->with($this->equalTo(array(
    'timestamp' => $date->format(FormatterInterface::DEFAULT_DATETIME_FORMAT) 
))); 

$mockDbDriver = $this->getMockBuilder('Zend\Db\Adapter\Driver\Pdo\Pdo') 
    ->disableOriginalConstructor() 
    ->getMock(); 

$mockDbAdapter = $this->getMock('Zend\Db\Adapter\Adapter', array(), array($mockDbDriver)); 
$mockDbAdapter->expects($this->once()) 
    ->method('query') 
    ->will($this->returnValue($mockStatement)); 

Próbowałem oddanie że do mojego sposobu setUp ale działa na phpunit Klasa testu daje mi następujący błąd:

Fatal error: Call to a member function createStatement() on null in C:\Program Files (x86)\Zend\Apache2\htdocs\test_project\vendor\zendframework\zend-db\src\Sql\Sql.php on line 128

Więc moje pytanie jest, w jaki sposób mock klasy w PHPUnit Zend\Db\Adapter\Adapter?

Widziałem this question, który jest podobny, ale zamiast tego używa Zend/Db/Adapter/AdapterInterface i nie mogę przetłumaczyć tego kodu na moją sytuację. Mapper i kod klasy testu poniżej.

ProductMapper.php:

public function __construct(Adapter $dbAdapter) { 
    $this->dbAdapter = $dbAdapter; 
    $this->sql = new Sql($dbAdapter); 
} 

public function fetchAllProducts() { 
    $select = $this->sql->select('products'); 

    $statement = $this->sql->prepareStatementForSqlObject($select); 
    $results = $statement->execute(); 

    $hydrator = new ClassMethods(); 
    $product = new ProductEntity(); 
    $resultset = new HydratingResultSet($hydrator, $product); 
    $resultset->initialize($results); 
    $resultset->buffer(); 

    return $resultset; 
} 

ProductMapperTest.php:

public function setUp() { 
    $date = new DateTime(); 

    $mockStatement = $this->getMock('Zend\Db\Adapter\Driver\Pdo\Statement'); 
    $mockStatement->expects($this->once())->method('execute')->with($this->equalTo(array(
      'timestamp' => $date->format(FormatterInterface::DEFAULT_DATETIME_FORMAT) 
    ))); 

    $mockDbDriver = $this->getMockBuilder('Zend\Db\Adapter\Driver\Pdo\Pdo')->disableOriginalConstructor()->getMock(); 

    $this->mockDbAdapter = $this->getMock('Zend\Db\Adapter\Adapter', array(), array(
      $mockDbDriver 
    )); 
    $this->mockDbAdapter->expects($this->once())->method('query')->will($this->returnValue($mockStatement)); 
} 

public function testFetchAllProducts() { 
    $resultsSet = new ResultSet(); 

    $productMapper = new ProductMapper($this->mockDbAdapter); 

    $this->assertSame($resultsSet, $productMapper->fetchAllProducts()); 
} 

EDIT # 1:

Obserwuj ing dalej od odpowiedzi Wilt za zmieniłem mapowania użyć klasy Sql w konstruktorze i zmieniłem klasę testową do:

public function setUp() { 
    $mockSelect = $this->getMock('Zend\Db\Sql\Select'); 

    $mockDbAdapter = $this->getMockBuilder('Zend\Db\Adapter\AdapterInterface')->disableOriginalConstructor()->getMock(); 

    $this->mockStatement = $this->getMock('Zend\Db\Adapter\Driver\Pdo\Statement'); 

    $this->mockSql = $this->getMock('Zend\Db\Sql\Sql', array('select', 'prepareStatementForSqlObject'), array($mockDbAdapter)); 
    $this->mockSql->method('select')->will($this->returnValue($mockSelect)); 
    $this->mockSql->method('prepareStatementForSqlObject')->will($this->returnValue($this->mockStatement)); 
} 

public function testFetchAllProducts() { 
    $resultsSet = new ResultSet(); 

    $this->mockStatement->expects($this->once())->method('execute')->with()->will($this->returnValue($resultsSet)); 

    $productMapper = new ProductMapper($this->mockSql); 

    $this->assertSame($resultsSet, $productMapper->fetchAllProducts()); 
} 

Ja jednak teraz pojawia się następujący błąd:

ProductTest\Model\ProductMapperTest::testFetchAllProducts Failed asserting that two variables reference the same object.

Która jest pochodzące z linii $this->assertSame($resultsSet, $productMapper->fetchAllProducts());. Czy wyśmiałem coś niepoprawnie?


Edit # 2:

Jak sugeruje Wilt, zmieniłem klasę testową użyć StatementInterface drwić oświadczenie zamiast, więc kod teraz wygląda:

public function setUp() { 

    $mockSelect = $this->getMock('Zend\Db\Sql\Select'); 

    $mockDbAdapter = $this->getMockBuilder('Zend\Db\Adapter\AdapterInterface')->disableOriginalConstructor()->getMock(); 

    $this->mockStatement = $this->getMock('Zend\Db\Adapter\Driver\StatementInterface'); 

    $this->mockSql = $this->getMock('Zend\Db\Sql\Sql', array('select', 'prepareStatementForSqlObject'), array($mockDbAdapter)); 
    $this->mockSql->method('select')->will($this->returnValue($mockSelect)); 
    $this->mockSql->method('prepareStatementForSqlObject')->will($this->returnValue($this->mockStatement)); 

} 

public function testFetchAllProducts() { 
    $resultsSet = new ResultSet(); 

    $this->mockStatement->expects($this->once())->method('execute')->with()->will($this->returnValue($resultsSet)); 

    $productMapper = new ProductMapper($this->mockSql); 

    $this->assertSame($resultsSet, $productMapper->fetchAllProducts()); 
} 

Ale przypadek testowy nadal nie działa tak, jak powyżej. Nie zmieniłem linii kodu, która kpi z metody execute, ponieważ uważam, że już powraca $resultsSet, ale mogę się mylić!

Odpowiedz

3

Może lepiej byłoby zmienić metodę __construct, aby pobrać instancję Sql jako argument. Wygląda na to, że $dbAdapter jest używany tylko wewnątrz konstruktora iz tego powodu wydaje mi się, że faktyczna zależność dla twojej klasy ProductMapper nie jest instancją Adapter, ale raczej instancją Sql. Jeśli wprowadzisz tę zmianę, musisz tylko wyśmiewać klasę Sql w swoim ProductMapperTest.

Jeśli nie chcesz dokonać takiej zmiany wewnątrz kodu i nadal chcesz kontynuować pisanie testu dla bieżącego ProductMapper klasy należy również Mock wszystkie inne metody klasy Adapter że klasa Sql dzwoni wewnętrznie.

Teraz nazywasz $this->sql->prepareStatementForSqlObject($select); na przykład Sql które wewnętrznie wywołuje metodę klasy AdaptercreateStatement (widać że here on line 128 inside the Sql class). Ale w twoim przypadku Adapter jest fałszywa i dlatego zostanie zgłoszony błąd:

Fatal error: Call to a member function createStatement() on null in C:\Program Files (x86)\Zend\Apache2\htdocs\test_project\vendor\zendframework\zend-db\src\Sql\Sql.php on line 128

Tak, aby rozwiązać ten problem należy drwić z tej metody zbyt podobnie do tego, jak to było z metodą query:

$mockStatement = //...your mocked statement... 
$this->mockDbAdapter->expects($this->once()) 
        ->method('createStatement') 
        ->will($this->returnValue($mockStatement)); 

W następnej linii dzwonisz pod numer $statement->execute();, co oznacza, że ​​będziesz musiał również wyśmiewać metodę execute w swoim $mockStatement.

Jak widzisz, ten test staje się dość kłopotliwy. I powinieneś zadać sobie pytanie, czy jesteś na dobrej drodze i testujesz właściwe komponenty. Możesz wprowadzić niewielkie zmiany w projekcie (ulepszenia), które ułatwiają testowanie klasy ProductMapper.

+0

Świetna odpowiedź dzięki, skierowała mnie we właściwym kierunku, zmieniając zależność, aby użyć instancji 'Sql'. Teraz pozbyłem się komunikatu o błędzie, ale sam test kończy się niepowodzeniem tam, gdzie oczekiwałbym tego. Dodałem nowy kod do pytania, czy nie miałbyś nic przeciwko temu, by rzucić okiem? Nie jestem pewien, czy wyśmiałam coś źle, czy jest to po prostu mój brak doświadczenia w testach jednostkowych i błędnie zakładam, że test przejdzie! – crazyloonybin

+0

@ Crazyloonybin Wystarczy użyć "StatementInterface", aby wyśmiać instrukcję (nie ma potrzeby, aby w tym celu była wymagana określona klasa adaptera). Następnie musisz wyśmiewać metodę 'execute' w tym' $ mockStatement', a wartość zwracana tej metody powinna być '$ resultsSet'. Następnie twój test powinien przejść. – Wilt

+0

Zmieniono instrukcję, aby przeczytać '$ this-> mockStatement = $ this-> getMock ('Zend \ Db \ Adapter \ Driver \ StatementInterface');' ale nadal otrzymuję ten sam błąd. Zachowałem kod '$ this-> mockStatement' taki sam jak to, co pokazano w edycji pytania, z funkcją' testFetchAllProducts' zwracającą '$ resultsSet' - czy ta linia również wymaga modyfikacji? – crazyloonybin