2009-07-29 84 views
6

我试图在个人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(); 
    } 
} 
+1

对于我来说,至少对单元测试来说,伟大之处在于它向您展示了您需要重构的地方。它还有助于突出显示依赖关系。我建议你的`SELECT`和你的`DELETE + INSERT`应该被重构到它们自己的方法中,密码的生成应该在它自己的方法中 – 2009-07-29 13:14:02

+0

@pcampbell - 你的评论应该是一个答案 – 2009-07-29 13:17:36

回答

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 

单元测试也是伟大的,因为它有助于突出的依赖。如上所示,此方法依赖于数据库,而不是实现接口的对象。该方法与超出范围的系统进行交互,并且只能通过集成测试进行测试,而不是单元测试。单元测试是为了确保工作单元的工作/正确性。考虑Single Responsibility Principle:“Do one thing”。它适用于方法和类。

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

  • 得到一个预先定义的现有用户对象/ ID。在这种方法之外做所有那些健康检查。做一件事。
  • 将密码重置代码放入其自己的方法中。这将是一个单独的工作单元,可以独立于SELECT,DELETEINSERT进行测试。
  • 创建一个可以称为OverwriteExistingPwdChangeRequests()的新方法,它可以处理DELETE + INSERT。
0

一般一个可能“模拟”要调用的对象,验证其接收的预期请求。

在这种情况下,我不确定这是多么有帮助,你amost最终编写了同样的逻辑两次......我们认为我们发送了“从密码删除”等哦看看我们没有!

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

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

1

你可以把它分解得更多一点,那个函数做的很多,这使得测试有点棘手,不是不可能,而是棘手。另一方面,如果你抽出了一些额外的小函数(getUserByUsername,deletePasswordByUserID,addPasswordByUserId等等),那么你可以轻松地测试一下这些函数,并知道它们的工作原理,所以你不必再次测试它们。确保它们是合理的,这样你就不用担心它们会进一步上链了,那么对于这个函数,你所需要做的就是抛出一个不存在的用户,并确保它返回一个USER_DOESNT_EXIST错误那么一个用户确实存在的地方(这是你测试DB的地方),内部工作已经在其他地方得到了实施(希望)

0

测试公共接口是必要的,但还不够。需要多少测试,我只能给出我自己的意见,测试一切,字面上,你应该有一个测试,验证每一行的代码已由测试套件执行。 (我只说'每一行',因为我在考虑C和gcov,而gcov提供了行级粒度,如果你有一个更高分辨率的工具,可以使用它。)如果你可以添加一段代码给你代码库而不添加测试,测试套件应该失败。

3

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

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

+0

“数据库的多少” “你需要执行以便能够检查这些状态变化吗?解析SQL?维护一些数据模仿表?我的观点是,使用真正的数据库最终会减少工作量。 – djna 2009-07-29 13:31:17

0

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

0

如果你的单元测试有副作用(如更改数据库),那么他们已经成为集成测试。集成测试本身没有任何问题;任何自动化测试都有利于您的产品质量。但是集成测试的维护成本更高,因为它们更复杂,更容易中断。

因此,最小化只能用副作用测试的代码。将SQL查询隔离并隐藏在不包含任何业务逻辑的独立MyDatabase类中。将此对象的实例传递给您的业务逻辑代码。

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

查看文档SimpleTest(一个php模拟框架)为例。

1

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

现在,这并不意味着您为每一行代码编写测试。这也不一定意味着你为每一个函数编写单元测试。决定是否测试某个特定的工作单元可能会导致风险。你有多愿意冒这样一段未经测试的代码被部署?

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