我应该在单元测试中走多远?

时间:2009-07-29 13:09:38

标签: unit-testing

我正在尝试在个人PHP项目中进行单元测试,就像一个优秀的小程序员,我想要正确地完成它。从我听到你应该测试的东西只是一个方法的公共接口,但我想知道这是否仍然适用于下面。

我有一个方法可以在用户忘记密码的情况下生成密码重置令牌。该方法返回以下两种情况之一:如果一切正常,则为nothing(null),或者表示具有指定用户名的用户不存在的错误代码。

如果我只测试公共接口,如果用户名有效,我如何确保密码重置令牌在数据库中进行,如果用户名无效,我怎么能不在数据库中?我应该在测试中进行查询以验证这一点吗?或者我应该假设我的逻辑是合理的?

现在这个方法非常简单,这不是什么大不了的事 - 问题在于同样的情况适用于许多其他方法。你在以数据库为中心的单元测试中做了什么?

代码,如有需要可供参考:

public function generatePasswordReset($username)
{
    $this->sql='SELECT  id
                FROM    users
                WHERE   username = :username';

    $this->addParam(':username', $username);
    $user=$this->query()->fetch();

    if (!$user)
        return self::$E_USER_DOESNT_EXIST;
    else
    {
        $code=md5(uniqid());
        $this->addParams(array(':uid'        => $user['id'],
                               ':code'       => $code,
                               ':duration'   => 24 //in hours, how long reset is valid
                              ));

        //generate new code, delete old one if present
        $this->sql ='DELETE FROM password_resets WHERE user_id=:uid;';
        $this->sql.="INSERT INTO password_resets (user_id, code, expires)
                     VALUES      (:uid, :code, now() + interval ':duration hours')";

        $this->execute();
    }
}

8 个答案:

答案 0 :(得分:6)

至少对我而言,单元测试的好处在于它向您展示了重构所需的位置。使用上面的示例代码,您基本上在一种方法中发生了四件事:

//1. get the user from the DB
//2. in a big else, check if user is null
//3. create a array containing the userID, a code, and expiry
//4. delete any existing password resets
//5. create a new password reset

单元测试也很棒,因为它有助于突出依赖性。如上所示,此方法依赖于DB,而不是实现接口的对象。此方法与其范围之外的系统交互,实际上只能使用集成测试进行测试,而不是单元测试。单元测试用于确保工作单元的工作/正确性。

考虑Single Responsibility Principle:“Do one thing”。它适用于方法和类。

我建议你的generatePasswordReset方法应该重构为:

  • 被赋予预定义的现有用户对象/ id。在这种方法之外做所有那些健全性检查。做一件事。
  • 将密码重置代码放入自己的方法中。这将是一个单独的工作单元,可以独立于SELECTDELETEINSERT进行测试。
  • 创建一个可以调用OverwriteExistingPwdChangeRequests()的新方法来处理DELETE + INSERT。

答案 1 :(得分:3)

此函数更难以进行单元测试的原因是因为数据库更新是函数的副作用(即没有明确的返回值供您测试)。

处理像这样的远程对象的状态更新的一种方法是创建一个模拟对象,该对象提供与DB相同的接口(即从代码的角度看它看起来相同)。然后在测试中,您可以检查此模拟对象中的状态更改,并确认您已收到应该使用的内容。

答案 2 :(得分:1)

你可以把它分解一下,这个功能很多,这使得测试有点棘手,不是不可能,但很棘手。另一方面,如果你拉出了一些较小的额外函数(getUserByUsername,deletePasswordByUserID,addPasswordByUserId等等)那么你可以很容易地测试一次并知道它们有效,所以你不必再测试它们。这样你就可以测试下面的向下调用以确保它们是正确的,所以你不必担心它们会进一步上升。然后对于这个函数,你需要做的就是抛弃一个不存在的用户并确保它返回USER_DOESNT_EXIST错误然后是用户确实存在的地方(这是测试数据库进入的地方)。内部工作已经在其他地方(希望)进行了。

答案 3 :(得分:1)

单元测试用于验证单元是否有效。如果您想知道某个单元是否有效,请编写一个测试。就这么简单。选择是否编写单元测试不应基于某些图表或经验法则。作为一名专业人士,您有责任提供工作代码,除非您进行测试,否则您无法知道它是否正常工作。

现在,这并不意味着您为每行代码编写测试。它也不一定意味着你为每一个函数编写一个单元测试。决定测试或不测试特定工作单元可归结为风险。您是否愿意承担部署未经测试的代码的风险?

如果你问自己“我怎么知道这个功能是否有效”,答案是“你没有,直到你有可重复的测试证明它有效”。

答案 4 :(得分:0)

通常,可以“模拟”您正在调用的对象,验证它是否收到了预期的请求。

在这种情况下,我不确定这有多大帮助,你最终写两次相同的逻辑...我们以为我们发送了“从密码中删除”等等哦看看我们做了!

嗯,我们实际检查了什么。如果字符串形成不好,我们就不知道了!

这可能违反了单位测试法的规定,但我会通过对数据库进行单独查询来测试这些副作用。

答案 5 :(得分:0)

测试公共接口是必要的,但还不够。关于需要多少测试,有很多哲学,我只能提出自己的看法。测试一切。从字面上看。您应该有一个测试来验证测试套件是否已经运行了每行代码。 (我只说'每一行',因为我正在考虑C和gcov,而gcov提供行级粒度。如果你有一个具有更好分辨率的工具,请使用它。)如果你可以添加一大块代码到你的代码库没有添加测试,测试套件应该失败。

答案 6 :(得分:0)

数据库是全局变量。 全局变量是公共接口,适用于使用它们的每个单元。因此,您的测试用例不仅必须改变函数参数的输入,还要改变数据库输入。

答案 7 :(得分:0)

如果您的单元测试有副作用(比如更改数据库),那么它们已成为集成测试。集成测试本身没有错;任何自动化测试都有利于您的产品质量。但集成测试具有<更高的维护成本,因为它们更复杂,更容易破解。

因此,诀窍是尽量减少只能用副作用测试的代码。在不包含任何业务逻辑的单独MyDatabase类中隔离和隐藏SQL查询。将此对象的实例传递给业务逻辑代码。

然后,当您对业务逻辑进行单元测试时,可以将MyDatabase对象替换为未连接到真实数据库的模拟实例,并且可以用于验证业务逻辑代码是否使用数据库正确。

有关示例,请参阅SimpleTest(php模拟框架)的文档。