我究竟如何为使用实时数据库的方法编写测试?
考虑以下代码:
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);
}
}
但如果数据或定价类型发生变化,我的测试也必须改变。
答案 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
类。如果数据在实时数据库中发生变化,则不会改变我的测试。