如何为使用实时数据库数据的方法编写PHP单元测试?

时间:2018-02-20 23:24:53

标签: php unit-testing phpunit php-7

我究竟如何为使用实时数据库的方法编写测试?

考虑以下代码:

class PricingRepository extends GenericRepository
{
    public function getOptionPrice(int $productId, int $quantity, float $productPrice = 0.0): float
    {
        //retrieves option record for a given product
        $row = $this->getMySql()->paramQuery("
            select * from pricing
            where product_id = ?", array(
            $productId
        ))->getSingleArray();

        //based on pricing type computes appropriate value
        if ($row['pricing_type'] === 'Quantity-based')
            return $row['base'] + $row['amount_per_quantity'] * $quantity;
        if ($row['pricing_type'] === 'Percentage-based')
            return $productPrice * $row['percentage'];

        throw new \InvalidArgumentException("invalid pricing type detected");
    }
}

我有很多像上面那样的方法,所以我想确保我的测试是可靠的,并且在数据库数据发生变化时不会改变。我正在寻找一流的单元测试方法的建议/解决方案,并且可能不依赖于数据库中的数据更改。

我现在可以编写一个天真的单元测试的方式可能是这样的:

use PHPUnit\Framework\TestCase;
class OptionPricingTest extends TestCase
{    
    function setUp()
    {
        $this->pricingRepository = new PricingRepository();
    }

    function testOptionPricing()
    { 
        $actual_option_price = $this->pricingRepository->getOptionPrice(111, 1, 100);
        $this->assertEquals(10.0, $actual_option_price);
    }
}

但如果数据或定价类型发生变化,我的测试也必须改变。

3 个答案:

答案 0 :(得分:3)

您的存储库的设计使其难以测试。

使用依赖注入

考虑不要在存储库类中创建数据库连接,而是通过构造函数注入它。

interface DBInterface
{
    public function paramQuery($query, array $params = []): DBInterface;
    public function getSingleArray(): array;
    // ...
}

class GenericRepository
{
    /** @var DBInterface */
    private $mysql;

    public function __construct(DBInterface $mysql)
    {
        $this->mysql = $mysql;
    }

    protected function getMySql(): DBInterface
    {
        return $this->mysql;
    }

    // ...
}

然后很容易注入一个模拟对象。

模拟依赖

对于上面的测试用例,模拟看起来像这样:

class MysqlMock implements DBInterface
{
    private $resultSet = [];
    private $currentQuery;
    private $currentParams;

    public function paramQuery($query, array $params = []): DBInterface
    {
        $this->currentId = array_shift($params);
    }

    public function getSingleArray(): array
    {
        return $this->resultSet[$this->currentId];
    }

    public function setResultSet($array records)
    {
        $this->resultSet = $records;
    }

    // ...
}

这样,您就可以独立于价格的实际变化和产品的移除。如果数据的结构发生变化,您只需更改测试。

use PHPUnit\Framework\TestCase;
class OptionPricingTest extends TestCase
{    
    private $pricingRepository;
    private $mysqlMock;

    public function setUp()
    {
        $this->mysqlMock         = new MysqlMock;
        $this->pricingRepository = new PricingRepository($mysqlMock);
    }

    public function testOptionPricing()
    { 
        $this->mysqlMock->setResultSet([
            111 => [
                'pricing_type'        => 'Quantity-based',
                'base'                => 6,
                'amount_per_quantity' => 4,
            ]
        ]);

        $actual_option_price = $this->pricingRepository->getOptionPrice(111, 1, 100);
        $this->assertEquals(10.0, $actual_option_price);
    }
}

答案 1 :(得分:1)

我是测试的初学者,所以这个答案可能不是(非常)有用甚至可能是错误的。如果是这种情况,请纠正我,这样我就可以从中学到一些东西:)

我想到的第一件事就是使用已知参数将条目插入python2.7: can't open file 'script.py first argument second argument': [Errno 2] No such file or directory 表(将插入行的id存储到pricing),然后获取使用:

$insertedRowID

然后像现在一样进行比较,但这样你就可以确定定价的类型和其他相关值是什么。对所有可能的(已知)方案重复相同的操作,以便验证所有情况都按预期工作。并且在测试之后从数据库中删除条目(使用创建它时存储的id)。

我用这种方法看到的问题是,有可能在测试期间(在添加新行之后和删除之前),可能会抛出异常或发生其他一些错误,这会导致在不会被删除的数据库条目中。如果是这种情况,我想更好的方法是以一种允许你使用模拟或间谍来模拟"数据库查询方法并始终返回您希望接收的值(并根据需要进行尽可能多的测试以涵盖可能来自数据库的所有变体)。

我还不熟悉使用嘲讽和间谍,以至于我可以很好地解释它们,所以希望有人带来更多经验,并对这个主题有所启发。

答案 2 :(得分:0)

使用https://github.com/phpspec/prophecy可以模拟一个类,而无需编写自己的模拟类。

例如,如果我在测试中使用真实数据库开始使用此行,我使用DI将$mysql注入到我的存储库类中。

$this->pricingRepository = new PricingRepository($mysql);

我可以使用$mysql

来模仿phpspec/prophecy
$sql = "
        select * from pricing
        where product_id = ?";
$data = [...]; // as returned by the database
$result = $this->prophesize(MySqlResult::class);
$mysql = $this->prophesize(MySql::class);
$mysql->paramQuery($sql, 111)->willReturn($result);

$this->pricingRepository = new PricingRepository($mysql->reveal());

精确模拟取决于$mysql课程的详细信息。你基本上构造了一个$mysql的模拟(包括任何支持类,例如我的MySqlResult},告诉它如何为你的特定用例行事。模拟框架完成剩下的工作。

我在这里测试的是我的PricingRepository类,它调用数据,并对数据进行一些计算。我测试那些计算,而我的模拟提供数据。实际上,我没有测试数据或数据库,我正在测试我的PricingRepository类。如果数据在实时数据库中发生变化,则不会改变我的测试。