如果测试涉及数据库(或网络、文件系统或任何其他外部服务),则它不是单元测试。除了任何定义之外,通过数据库进行测试很慢并且容易出错。模拟在动态语言中几乎太容易了,但许多人仍然通过数据库测试业务逻辑。我通常不喜欢发布问题的完整解决方案。在这种情况下,我觉得有必要这样做以证明它实际上很容易,尤其是在问题中描述的情况下。
class CheckBruteTest extends PHPUnit_Framework_TestCase {
public function test_checkbrute__some_user__calls_db_and_statement_with_correct_params() {
$expected_user_id = 23;
$statement_mock = $this->getMock('StatementIface', array());
$statement_mock->expects($this->once())->method('bind_param')
->with($this->equalTo('i'), $this->equalTo($expected_user_id));
$statement_mock->expects($this->once())->method('execute');
$statement_mock->expects($this->once())->method('store_result');
$db_mock = $this->getMock('DbIface', array());
$time_ignoring_the_last_two_decimals = floor((time() - 2 * 60 * 60) / 100);
$db_mock->expects($this->once())->method('prepare')
->with($this->stringStartsWith("SELECT time FROM login_attempts WHERE user_id = ? AND time > '$time_ignoring_the_last_two_decimals"))
->will($this->returnValue($statement_mock));
checkbrute($expected_user_id, $db_mock);
}
public function test_checkbrute__more_then_five__return_true() {
$statement_mock = $this->getMock('StatementIface', array());
$statement_mock->num_rows = 6;
$db_mock = $this->getMock('DbIface', array());
$db_mock->expects($this->once())->method('prepare')
->will($this->returnValue($statement_mock));
$result = checkbrute(1, $db_mock);
$this->assertTrue($result);
}
public function test_checkbrute__less_or_equal_then_five__return_false() {
$statement_mock = $this->getMock('StatementIface', array());
$statement_mock->num_rows = 5;
$db_mock = $this->getMock('DbIface', array());
$db_mock->expects($this->once())->method('prepare')
->will($this->returnValue($statement_mock));
$result = checkbrute(1, $db_mock);
$this->assertFalse($result);
}
}
interface DbIface {
public function prepare($query);
}
abstract class StatementIface {
public abstract function bind_param($i, $user_id);
public abstract function execute();
public abstract function store_result();
public $num_rows;
}
由于我不知道函数的规范,我只能从代码中得出它。
在第一个测试用例中,我只检查数据库的模拟以及代码是否按预期实际调用这些服务的语句。最重要的是,它检查是否将正确的用户 ID 传递给语句以及是否使用了正确的时间限制。时间检查相当麻烦,但这就是你得到的,如果你time()
直接在你的业务代码中调用服务。
第二个测试用例强制尝试登录的次数为 6 并断言函数是否返回true
。
第三个测试用例强制尝试登录的次数为 5 并断言函数是否返回false
。
这涵盖了(几乎)函数中的所有代码路径。只有一个代码路径被遗漏:如果$mysqli->prepare()
返回 null 或任何其他计算为false
整个 if 块的值被绕过并隐式返回 null。我不知道这是不是故意的。代码应该使类似的事情变得明确。
出于嘲笑的原因,我创建了小接口和小抽象类。它们仅在测试的上下文中需要。也可以为 的$mysqli
参数和返回值实现自定义模拟类$mysqli->prepare()
,但我更喜欢使用自动模拟。
一些与解决方案无关的附加说明:
- 单元测试是开发人员测试,应该由开发人员自己编写,而不是一些糟糕的测试人员。测试人员编写验收和回归测试。
- 测试用例的“hackiness”说明了为什么在事后编写测试更加困难。如果开发人员编写了 TDD 风格的代码,代码和测试会更加简洁。
- checkbrute 函数的设计相当不理想:
- “checkbrute”是个坏名字。它并没有真正讲述它的故事。
- 它将业务代码与数据库访问混合在一起。时间约束的计算是业务代码以及对
>5
. 中间的代码是数据库代码,属于它自己的函数/类/任何东西。
- 魔术数字。请为幻数使用常量,例如 2h 值和最大登录尝试次数。