Database unit test bound to implementation detail

2019-04-11 05:54发布

问题:

I have a simple PHP-Class that wraps the access to the database to retrieve a user and want to unit-test it. I currently have the following code:

The class to test:

class UserTable {

    protected $tableGateway;

    public function __construct(\Zend\Db\TableGateway\TableGateway $tableGateway) {
        $this->tableGateway = $tableGateway;
    }

    public function getUserWithId($id) {
        return $this->tableGateway->select(['id' => $id])->current();
    }
}

The unit test:

class UserTableTest extends \PHPUnit_Framework_TestCase {
    public function testGetUserWithIdReturnsCorrectUser() {
        $user = new User();

        $resultSet = new ResultSet();
        $resultSet->initialize([$user]);

        $mockTableGateway = $this->getMock('\Zend\Db\TableGateway\TableGateway', ['select'], [], '', false);
        $mockTableGateway->expects($this->once())->method('select')->with(['id' => 1])->willReturn($resultSet);

        $userTable = new UserTable($mockTableGateway);

        $this->assertEquals($user, $userTable->getUserWithId(1));
    }
}

However, now the unit test would fails if I later decided to change the way I use the table gateway (e.g. use select(['id = ?' => $id]). This binds the unit test to an implementation detail of getUserWithId($id) which should be avoided.

What would be best practice to prevent the unit test from depending on an implementation detail? Is it worth the effort to set up an actual testing database that the unit test can run against (which will also slow down the execution of the test dramatically) or is there a better way to mock the table gateway?

回答1:

Do not mock code that you don't own!* For classes that use database, you have to write integration tests. The good things is that this will force you to separate the DB access from other logic.

*This is actual advice from the "Growing object-oriented software, guided by tests" book, backed up by my own experience with writing tests for code that uses Doctrine's Entity Manager