在Laravel中使用测试工厂是一种反模式吗?

时间:2018-12-25 00:00:13

标签: laravel integration-testing anti-patterns

在此幻灯片上:

enter image description here

Sandiz Metz talks about how we should test from the outside,但不了解SUT内部的内容。

我认为她的演讲主要集中在单元测试上,而不是集成测试上,而是让我怀疑……在Laravel中使用factories是否可以成为反模式?

在我看来,使用这些工厂意味着我知道数据库数据必须是SUT才能完成其任务。

例如,假设用户必须能够编辑其个人资料。有了工厂,我可以做到:

/** @test */
public function the_user_can_update_his_profile()
{
    $user = factory(User::class)->create();

    // ACT

    // ASSERT
}

但是,这些知识似乎太过深入和详尽了。我必须知道如何创建正确注册的用户。遵循留在外界的想法,我是否应该使用已经存在的对象为测试准备数据?

/** @test */
public function the_user_can_update_his_profile()
{
    $userRepository = app(UserRepository::class);
    $user = $userRepository->register('email@email.com', 'password123');

    // ACT

    // ASSERT
}

更进一步,我怎么知道这是正确的方法?我不应该简单地调用用户用于注册的路由吗?

/** @test */
public function the_user_can_update_his_profile()
{
    $response = $this->json('POST', route('register_user'), ['email' => 'email@email.com', 'password' => 'password123']);
    $userRepository = app(UserRepository::class);
    $user = $userRepository->find($response['userId']);

    // ACT

    // ASSERT
}

但是这种(极端)解决方案还可以做很多不必要的其他事情(例如发送确认电子邮件)。它还需要注册路由才能工作。

您认为什么是最复杂的项目的最干净的解决方案?

1 个答案:

答案 0 :(得分:0)

如评论中所述,您误认为了单元测试集成测试的目的和范围,由此带来的困惑。让我们从“测试”开始:

/* IN HEADER */
class ComplexNumber
{
private:
    double m_real, m_imaginary;

public:
    ComplexNumber(double real = 0.0, double imaginary = 0.0);
    ComplexNumber(const ComplexNumber &obj);

    ComplexNumber& operator= (const ComplexNumber &obj);

    operator int();

};

ComplexNumber::ComplexNumber(double real, double imaginary): 
    m_real(real), m_imaginary(imaginary)
{

}

ComplexNumber::ComplexNumber(const ComplexNumber &obj): 
m_real(obj.m_real), m_imaginary(obj.m_imaginary)
{

}

ComplexNumber& ComplexNumber::operator= (const ComplexNumber &obj)
{
    m_real = obj.m_real;
    m_imaginary = obj.m_imaginary;

    return *this;
}

ComplexNumber::operator int()
{
    m_real = static_cast<int>(m_real);
    m_imaginary = static_cast<int>(m_imaginary);

    return *this;
}

/* IN MAIN */
ComplexNumber obj(3.4, 5.6);

obj = static_cast<int>(obj);
//here it gives seg fault

这是一个集成测试。您正在测试“远程”路由(好的,它是内部的,但是您仍在测试过程中的每个组件)。集成测试非常适合确认业务逻辑是否正确实施以及路由是否符合预期;它贯穿了从头到尾的整个过程,测试了组件之间以及控制器+视图本身中的逻辑之间的每个交互。

这一切都很好,但是并不能真正帮助我们。对于组件本身的可测试性,我们仍然是盲目的。这是进行单元测试的地方。

假设您有一个如下课程:

/** @test */
public function the_user_can_update_his_profile()
{
    $response = $this->json('POST', route('register_user'), ['email' => 'email@email.com', 'password' => 'password123']);
    $userRepository = app(UserRepository::class);
    $user = $userRepository->find($response['userId']);

    // ACT

    // ASSERT
}

您很可能最终在一个控制器中使用了此类。进行单元测试的目的是断言:

  • 构造函数确实存储正确的值(作为整数)
  • <?php class Foo { public $value = 0; public function __construct($value) { $this->value = (int)$value; } public function getRemainder(int $item) { return $this->value % $item; } } 方法在罐子上说的话。

为此,我们可以编写以下测试:

isModulo

当然,我们已经独立测试了组件的每个方法,每个分支。这是单元测试。

与与其他对象进行交互的类会变得更加混乱,但是,如果结构正确,则很容易利用工具注入对象的模拟副本,以便能够进行交互。当您的代码以一种不整洁的方式同时触及多个事物时,您的测试往往会变得一团糟。

您链接的话题是关于这些测试的,以及使它们不成为容易陷入困境的代码库中的脆弱混乱的方法。从理论上讲,作者也是正确的-您从没有依赖关系的对象开始,进行测试,然后假定这些对象建立的 contracts 是健全且经过测试的,然后逐步进行,然后逐步向上金字塔。在实践中,事后考虑将代码编写为测试是非常困难的。