如何为特定的php函数设置测试

时间:2013-05-06 13:57:07

标签: php unit-testing testing tdd phpunit

任何人都可以帮我设置这个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;
      }
   }
}

2 个答案:

答案 0 :(得分:0)

如果测试正在触及数据库(或网络,文件系统或任何其他外部服务),则它不是单元测试。除了任何定义,通过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;
}

由于我不知道函数的规范,我只能从代码中派生出来。

在第一个测试用例中,我只检查数据库的模拟和语句,如果代码实际上是按预期调用这些服务。最重要的是,它检查是否将正确的用户ID传递给语句,以及是否使用了正确的时间约束。时间检查相当苛刻,但如果您直接在业务代码中调用time()等服务,那就是您所获得的。

第二个测试用例强制尝试登录的次数为6,如果函数返回true则断言。

第三个测试用例强制尝试登录的次数为5,如果函数返回false则断言。

这涵盖了(几乎)函数中的所有代码路径。只剩下一个代码路径:如果$mysqli->prepare()返回null或任何其他值为false的值,则绕过整个if块并隐式返回null。我不知道这是否有意。代码应该做出类似的陈述。

出于模拟的原因,我创建了小接口和小抽象类。它们仅在测试环境中需要。也可以为$mysqli参数和$mysqli->prepare()的返回值实现自定义模拟类,但我更喜欢使用自动模拟。

一些与解决方案无关的补充说明:

  • 单元测试是开发人员测试,应该由开发人员自己编写,而不是一些不好的测试人员。测试人员编写接受和回归测试。
  • 测试用例的“hackiness”表明为什么在事实之后编写测试要困难得多。如果开发人员编写代码TDD样式,则代码和测试会更加清晰。
  • checkbrute函数的设计相当不理想:
    • 'checkbrute'是一个坏名字。它并没有真正讲述它的故事。
    • 将业务代码与数据库访问混合在一起。时间约束的计算是业务代码以及>5的检查。中间的代码是DB代码,属于自己的函数/ class / whatever。
    • 魔术数字。请使用常数作为幻数,例如2h值和最大登录尝试次数。

答案 1 :(得分:-1)

单元测试很容易创建:它将数据放入数据库(带插入),并检查函数的返回是否对这些数据有效。然后,当测试完成后,它会删除数据。

在这里,您必须在login_attempts中插入值,例如:

INSERT login_attempts(user_id, time, ...) VALUES (12, 1367849298)

然后检查函数的返回值。