PHP-主体中包含不可修改类的单元测试方法(PHPUnit)

PHP-主体中包含不可修改类的单元测试方法(PHPUnit),php,unit-testing,eloquent,phpunit,Php,Unit Testing,Eloquent,Phpunit,我有一个使用另一个类来计算结果的方法,我想用PHPUnit来测试它 /** * Returns true if the given user has been granted the given permission. * * @param User $user * @param AbstractPermission $permission * @return bool */ public function userPermissionGranted(User $user, Abst

我有一个使用另一个类来计算结果的方法,我想用PHPUnit来测试它

/**
 * Returns true if the given user has been granted the given permission.
 *
 * @param User $user
 * @param AbstractPermission $permission
 * @return bool
 */
public function userPermissionGranted(User $user, AbstractPermission $permission) : bool
{
    // Retrieve model from database.
    $user_permission = UserPermission::scopeUser($user)
        ->scopePermission($permission)
        ->first();

    return $user_permission ? $user_permission->isGranted() : $permission->isGrantedByDefault();
}
不考虑这个方法的实际作用,我想知道如何测试这个方法。我可以将
User
AbstractPermission
类的模拟传递给方法,但是在方法主体内部使用的
UserPermission
类(用于从数据库检索模型)我无能为力

最重要的是,如果我通过对
User
Permisson
类的模拟,它们将不存在于数据库中,因此当
UserPermission
查询数据库时,它将不会收到任何结果,方法将失败

我在这里干什么?简单地模拟数据库(即复制live db结构并用测试数据填充它)并让我的模型查询该数据库,并且只相信一切正常,这被认为是一种良好的做法吗?有什么建议吗


另一方面,
UserPermission
是一个雄辩的模型。我只是在这里使用雄辩的方法——没有Laravel。

一般来说,你不能直接模仿静态方法——至少,没有好的方法。根据应用程序的设置方式,您可能可以一起破解一些东西,包括使用runkit重新定义方法,或者可能使用includes/autoloader加载模拟类,而不是真实类,但这样的解决方案充其量也很麻烦

允许单元测试的一种简单方法是将静态方法调用封装在实例方法中。因此,您将使用调用静态方法的实例方法创建一个新类。当然,您无法测试这个新类,但是如果它是静态方法的薄包装,那么测试它就没有任何价值

所以你可能会得到这样的结果,例如:

class UserPermissionWrapper {
    public function getUserPermission($user) {
        return UserPermission::scopeUser($user);
    }
}
然后,您可以将其注入到原始类中,并获得如下内容:

public function userPermissionGranted(User $user, AbstractPermission $permission) : bool
{
    // Assume this is an instance of UserPermissionWrapper injected at construction
    $user_permission = $this->userPermissionWrapper
        ->getUserPermission($user)
        ->scopePermission($permission)
        ->first();

    return $user_permission ? $user_permission->isGranted() : $permission->isGrantedByDefault();
}

现在您有了一个调用实例方法的对象,因此您可以注入该类的模拟版本,并以正常方式设置方法调用。

为了回答我自己的问题-我只是在编写了一些更多的单元测试之后才得出合理的答案-我想归结起来是这样的:


在测试
userpermissiongrated()
方法时,我们实际上只是验证该方法是否按预期工作。我们正在从数据库中获取一个模型,我们可以假设这个模型已经在它自己的单独测试中进行了测试。考虑到我们可能假设此模型按预期工作,并且我们无法访问此处的数据库以获取实际模型,我们可以使用模型的模拟,我们习惯于按照其工作方式构建模型,而不实际执行任何数据库工作。这就是彼得·吉尔的答案。我们的类应该包含一个从数据库获取模型的方法,这样我们就可以设置一个mock并返回模型,而不是从数据库获取并返回模型。在这种情况下,这意味着在我们正在测试的方法的
返回
行中,我们将在
$user\u permission
(我们创建的模拟是为了返回我们希望它返回的值),
isgrantedbyderdefault()上测试一个模拟的
isgrantedbynetd()
在调用方法时传递给该方法的
$permission
mock上。

谢谢!这是一个有用的答案。只有一件事我还在想。从技术上讲,我的方法是测试数据库记录是否存在,如果存在,则返回x,如果不存在,则返回y。在单元测试中,我是否应该简单地不测试所有与数据库相关的方法,而只是假设所有这些方法都正常工作?(我的意思是,不管怎样,这就是模拟类/方法时所做的,对吗?)(也许我是指只做与数据库相关的事情并返回void的方法)我想说,测试与数据库的实际交互更像是一个集成测试。拥有一个与测试数据库对话的集成测试套件是一件好事,但与单元测试套件相比,它的设置和维护工作量更大。这就是为什么单元测试得到如此多的重视——它们提供了很多价值,而且编写起来相对便宜。对于您的情况,由于您使用的是第三方ORM,我想说,如果您的包装足够薄,那么就没有必要测试它们。ORM应该已经由它的维护人员进行了测试,重复这项工作没有任何价值。