谁能帮我设置这个php代码的单元测试,我一直在尝试,还没有找到解决方案。我从开发团队收到了这个代码,我应该为它创建单元测试,关于如何做到这一点的任何想法?
function checkbrute($user_id, $mysqli) {
// Get timestamp of current time
$now = time();
// All login attempts are counted from the past 2 hours.
$valid_attempts = $now - (2 * 60 * 60);
if ($stmt = $mysqli->prepare("SELECT time FROM login_attempts WHERE user_id = ? AND time > '$valid_attempts'")) { `enter code here`
$stmt->bind_param('i', $user_id);
// Execute the prepared query.
$stmt->execute();
$stmt->store_result();
// If there has been more than 5 failed logins
if($stmt->num_rows > 5) {
return true;
} else {
return false;
}
}
}
如果测试触及数据库(或网络、文件系统或任何其他外部服务),它就不是单元测试。除了任何定义之外,通过DB进行测试是缓慢且容易出错的。在动态语言中,模拟几乎太容易了,但是许多人仍然通过DB测试业务逻辑。我通常不喜欢对一个问题发布完整的解决方案。在这种情况下,我觉得有必要这样做,以证明它实际上是很容易的,特别是在问题所描述的情况下。
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;
}
由于我不知道函数的规范,我只能从代码中派生它。
在第一个测试用例中,我只检查了DB和语句的模拟,如果代码实际上按预期调用了这些服务。最重要的是,它检查是否向语句传递了正确的用户ID,以及是否使用了正确的时间约束。时间检查相当粗糙,但如果您直接在业务代码中调用time()
之类的服务,则会得到这样的检查。
第二个测试用例强制尝试登录的次数为6,并断言函数是否返回true
。
第三个测试用例强制尝试登录的次数为5,并断言函数是否返回false
。
这涵盖了函数中(几乎)所有的代码路径。只有一个代码路径被遗漏:如果$mysqli->prepare()
返回null或计算false
的任何其他值,则绕过整个If块并隐式返回null。我不知道这是不是故意的。代码应该使这样的内容显式。
出于嘲弄的原因,我创建了一个小接口和一个小抽象类。只有在测试的上下文中才需要它们。也可以为$mysqli
参数和$mysqli->prepare()
的返回值实现自定义模拟类,但我更喜欢使用自动模拟。
一些与解决方案无关的附加注释:
- 单元测试是开发人员测试,应该由开发人员自己编写,而不是由一些糟糕的测试人员编写。测试人员编写验收和回归测试。
- 测试用例的"粗糙性"说明了为什么事后编写测试要困难得多。如果开发者编写TDD风格的代码,那么代码和测试将会更加清晰。
- checkbrute函数的设计是相当不理想的:
- 'checkbrute'是个坏名字。它并没有真正地讲述它的故事。
- 它是混合业务代码与数据库访问。时间约束的计算是业务代码以及
>5
的检查。两者之间的代码是DB代码,属于它自己的函数/类/等等。 - 神奇的数字。请使用常量作为幻数,如2h值和最大登录尝试数。
创建单元测试很简单:它将数据放入数据库(使用insert),并检查函数对这些数据的返回是否正确。然后,当测试完成后,它删除数据。
在这里,您必须在login_attempts中插入值,例如:
INSERT login_attempts(user_id, time, ...) VALUES (12, 1367849298)
,然后检查函数的返回值