Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Zend Framework 2 & PHPUnit - mock the Zend\Db\Adapter\Adapter class

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!

like image 903
crazyloonybin Avatar asked Feb 09 '17 10:02

crazyloonybin


1 Answers

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 in C:\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.

like image 182
Wilt Avatar answered Nov 25 '22 14:11

Wilt