I started learning Zend Framework a couple of years ago following this tutorial. In there, it shows mappers are created using the Zend\Db\Adapter\Adapter
class to get the database connection, and that is how I've worked with databases since with no issues.
I'm now trying to learn how to use PHPUnit on Zend applications, and am running into difficulties in testing the functions in the mapper as I'm unable to mock the Zend\Db\Adapter\Adapter
class.
This tutorial on the Zend website shows mocking database connections, but it uses the Zend\Db\TableGateway\TableGateway
class. All other tutorials I've found online use this class too, and the only thing I've found regarding the Zend\Db\Adapter\Adapter
class is 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));
I've tried putting that into my setUp
method but running phpunit
on the test class gives me the following error:
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
So my question is, how do you mock the Zend\Db\Adapter\Adapter
class in PHPUnit?
I've seen this question which is similar, but uses a Zend/Db/Adapter/AdapterInterface
instead, and I can't seem to translate that code into my situation. Mapper and test class code below.
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:
Following on from Wilt's answer, I changed my mapper to use the Sql
class in the constructor and changed my Test class to:
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());
}
However, I now get the following error:
ProductTest\Model\ProductMapperTest::testFetchAllProducts Failed asserting that two variables reference the same object.
Which is coming from the line $this->assertSame($resultsSet, $productMapper->fetchAllProducts());
. Have I mocked something incorrectly?
Edit #2:
As suggested by Wilt, I changed the test class to use a StatementInterface
to mock a statement instead, so the code now looks like:
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());
}
But the test case is still failing as above. I haven't changed the line of code which is mocking the execute
method as I believe it was already returning $resultsSet
, however I could be wrong!
Maybe it would be better here to change the __construct
method to take an Sql
instance as an argument. It seems like the $dbAdapter
is only used inside the constructor and because of this it seems to me that the actual dependency for your ProductMapper
class is not an Adapter
instance, but rather a Sql
instance. If you make that change you only need to mock the Sql
class inside your ProductMapperTest
.
If you don't want to make such a change inside your code and you still want to continue writing a test for the current ProductMapper
class you should also mock all the other methods of the Adapter
class that the Sql
class is calling internally.
Right now you call $this->sql->prepareStatementForSqlObject($select);
on your Sql
instance which internally calls the createStatement
method of the Adapter
class (you can see that here on line 128 inside the Sql
class). But in your case the Adapter
is a mock and that is why the error is thrown:
Fatal error: Call to a member function
createStatement()
on null inC:\Program Files (x86)\Zend\Apache2\htdocs\test_project\vendor\zendframework\zend-db\src\Sql\Sql.php
on line 128
So to solve this you should mock this method too similarly to how you did for the query
method:
$mockStatement = //...your mocked statement...
$this->mockDbAdapter->expects($this->once())
->method('createStatement')
->will($this->returnValue($mockStatement));
In the next line you call $statement->execute();
meaning you will also need to mock the execute
method inside your $mockStatement
.
As you see the writing this test becomes pretty cumbersome. And you should ask yourself if you are on the right path and testing the right components. You could make some small design changes (improvements) that makes it easier to test your ProductMapper
class.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With