如何测试与数据库互动的代码

3.4 版本
维护中的版本

如果你的代码与数据库互动,比如,从中读取数据或向其写入数据,你需要考虑到这点来调整测试(代码)。如何处理这个问题有多种方式。在单元测试中(unit test),你可以为 Repository 创建一个mock,并使用它来返回预期的对象。在功能测试中(functional test),你可能需要准备一个“带有预置值”的数据库来确保你的测试始终拥有能够使用的“相同数据”。

若你需要直接测试查询,见 如何测试Doctrine Repositories

在单元测试中模拟Repository 

如果你希望基于独立的Doctrine repository来测试代码,你需要mock Repository。通常你要注入 EntityManager 到你的类中,并使用它来获取repository。这就令事情有些棘手因为你需要同时mock EntityManager 和你的宝库类。

把你的repository作为 factory service 注册(到容器中)再行注入之是可以的(并且非常好)。这在设置上略繁杂,但却令测试变容易,因为你只需mock那个repository即可。

假设你希望测试的类是下面这种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/AppBundle/Salary/SalaryCalculator.php
namespace AppBundle\Salary;
 
use Doctrine\Common\Persistence\ObjectManager;
 
class SalaryCalculator
{
    private $entityManager;
 
    public function __construct(ObjectManager $entityManager)
    {
        $this->entityManager = $entityManager;
    }
 
    public function calculateTotalSalary($id)
    {
        $employeeRepository = $this->entityManager
            ->getRepository('AppBundle:Employee');
        $employee = $employeeRepository->find($id);
 
        return $employee->getSalary() + $employee->getBonus();
    }
}

由于 ObjectManager 是通过构造器注入到类中的,因此在测试中,传给它一个模拟对象(a mock object)是很容易的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// tests/AppBundle/Salary/SalaryCalculatorTest.php
namespace Tests\AppBundle\Salary;
 
use AppBundle\Entity\Employee;
use AppBundle\Salary\SalaryCalculator;
use Doctrine\ORM\EntityRepository;
use Doctrine\Common\Persistence\ObjectManager;
 
class SalaryCalculatorTest extends \PHPUnit_Framework_TestCase
{
    public function testCalculateTotalSalary()
    {
        // First, mock the object to be used in the test
        // 首先,模拟用在测试中的对象
        $employee = $this->createMock(Employee::class);
        $employee->expects($this->once())
            ->method('getSalary')
            ->will($this->returnValue(1000));
        $employee->expects($this->once())
            ->method('getBonus')
            ->will($this->returnValue(1100));
 
        // Now, mock the repository so it returns the mock of the employee
        // 现在,模拟repository令其返回employee的模拟
        $employeeRepository = $this
            ->getMockBuilder(EntityRepository::class)
            ->disableOriginalConstructor()
            ->getMock();
        $employeeRepository->expects($this->once())
            ->method('find')
            ->will($this->returnValue($employee));
 
        // Last, mock the EntityManager to return the mock of the repository
        // 最后,模拟EntityManager以返回repository的模拟 
        $entityManager = $this
            ->getMockBuilder(ObjectManager::class)
            ->disableOriginalConstructor()
            ->getMock();
        $entityManager->expects($this->once())
            ->method('getRepository')
            ->will($this->returnValue($employeeRepository));
 
        $salaryCalculator = new SalaryCalculator($entityManager);
        $this->assertEquals(2100, $salaryCalculator->calculateTotalSalary(1));
    }
}

本例中,你“从内而外”地构建了mocks,先创建了由 Repository 所返回的“雇员”(employee),它本身又是通过 EntityManager 来返回。在这种方式下,并没有真实的类被郑入测试。

为功能测试改变数据库配置 

如果你有功能测试,你需要令其与真实数据库互动。多数情况下,你需要使用一个专门的数据库连接以确保不去覆写你在开发过程中(已经)输入的数据,同时还要在每次测试之前“能够清除数据库”。

要这样做,你可以指定一个数据库配置,去覆写默认的配置:

1
2
3
4
5
6
7
8
# app/config/config_test.yml
doctrine:
    # ...
    dbal:
        host:     localhost
        dbname:   testdb
        user:     testdb
        password: testdb
1
2
3
4
5
6
7
8
9
<!-- app/config/config_test.xml -->
<doctrine:config>
    <doctrine:dbal
        host="localhost"
        dbname="testdb"
        user="testdb"
        password="testdb"
    />
</doctrine:config>
1
2
3
4
5
6
7
8
9
// app/config/config_test.php
$configuration->loadFromExtension('doctrine', array(
    'dbal' => array(
        'host'     => 'localhost',
        'dbname'   => 'testdb',
        'user'     => 'testdb',
        'password' => 'testdb',
    ),
));

确保你的数据库运行在localhost,并且定义了数据库名称和凭据等配置。

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)