繁体   English   中英

TDD:最佳实践 mocking 对象堆栈

[英]TDD: best practices mocking stacks of objects

我正在尝试熟悉 PHP 中的单元测试和 Lumen 中的小 API。 在一些教程的帮助下编写前几个测试非常好,但现在我遇到了必须模拟/存根依赖项的问题。

我的 controller 取决于构造函数中提示的特定自定义接口类型。
当然,我在 ServiceProvider 中定义了这个接口/实现绑定。

    public function __construct(CustomValidatorContract $validator)
    {
        // App\Contracts\CustomValidatorContract
        $this->validator = $validator;
    }

    public function resize(Request $request)
    {
        // Illuminate\Contracts\Validation\Validator
        $validation = $this->validator->validate($request->all());

        if ($validation->fails()) {
            $response = array_merge(
                $validation
                ->errors() // Illuminate\Support\MessageBag
                ->toArray(), 
                ['error' => 'Invalid request data.']
            );

            // response is global helper
            return response()->json($response, 400, ['Content-Type' => 'application/json']);
        }
    }

如您所见,我的CustomValidatorContract有一个validate()方法,它返回一个Illuminate\Contracts\Validation\Validator实例(验证结果)。 当调用errors()时,这又会返回Illuminate\Support\MessageBag的实例。 MessageBag然后有一个toArray()方法。

现在我想测试我的 controller 的行为,以防验证失败。

    /** @test */
    public function failing_validation_returns_400()
    {
        $EmptyErrorMessageBag = $this->createMock(MessageBag::class);
        $EmptyErrorMessageBag
            ->expects($this->any())
            ->method('toArray')
            ->willReturn(array());

        /** @var ValidationResult&\PHPUnit\Framework\MockObject\MockObject $AlwaysFailsTrueValidationResult */
        $AlwaysFailsTrueValidationResult = $this->createStub(ValidationResult::class);
        $AlwaysFailsTrueValidationResult
            ->expects($this->atLeastOnce())
            ->method('fails')
            ->willReturn(true);
        $AlwaysFailsTrueValidationResult
            ->expects($this->atLeastOnce())
            ->method('errors')
            ->willReturn($EmptyErrorMessageBag);

        /** @var Validator&\PHPUnit\Framework\MockObject\MockObject $CustomValidatorAlwaysFailsTrue */
        $CustomValidatorAlwaysFailsTrue = $this->createStub(Validator::class);
        $CustomValidatorAlwaysFailsTrue
            ->expects($this->once())
            ->method('validate')
            ->willReturn($AlwaysFailsTrueValidationResult);

        $controller = new ImageResizeController($CustomValidatorAlwaysFailsTrue);
        $response = $controller->resize(new Request);

        $this->assertEquals(400, $response->status());
        $this->assertEquals(
            'application/json',
            $response->headers->get('Content-Type')
        );
        $this->assertJson($response->getContent());
        $response = json_decode($response->getContent(), true);
        $this->assertArrayHasKey('error', $response);
    }

这是一个运行良好的测试 - 但是有人可以告诉我是否有更好的方法来编写它吗? 感觉不对。 由于我在后台使用框架,是否需要这么大堆 moc 对象? 还是我的架构有问题,以至于感觉如此“过度设计”?

谢谢

您所做的不是单元测试,因为您没有测试应用程序的单个单元。 这是一个集成测试,使用单元测试框架执行,这就是它看起来直观错误的原因。

单元测试和集成测试发生在不同的时间、不同的地方,并且需要不同的方法和工具——前者测试代码的每一个 class 和 function,而后者并不关心这些,他们只是请求 API 并验证响应。 此外,IT 并不意味着 mocking 任何事情,因为它的目标是测试您的单元之间的集成程度。

您将很难支持这样的测试,因为每次更改CustomValidatorContract时,您都必须修复所有涉及它的测试。 这就是 UT 如何通过要求它尽可能松耦合来改进代码设计(因此您可以选择一个单元并使用它而无需启动整个应用程序),尊重SRPOCP等。

您不需要测试第 3 方代码,而是选择一个已经测试过的代码。 您也不需要测试副作用,因为环境就像 3rd 方服务,应该单独测试( return response()是副作用)。 它还严重减慢了测试速度。

所有这些导致您只想单独测试您的CustomValidatorContract的想法。 你甚至不需要在那里模拟任何东西,只需实例化验证器,给它几组输入数据并检查它是如何进行的。

这是一个运行良好的测试 - 但是有人可以告诉我是否有更好的方法来编写它吗? 感觉不对。 由于我在后台使用框架,是否需要这么大堆 moc 对象? 还是我的架构有问题,以至于感觉如此“过度设计”?

大量的模拟对象表明您的测试对象与许多不同的事物紧密耦合。

如果您想支持更简单的测试,那么您需要使设计更简单。

换句话说,不是Controller.resize是一个了解所有细节的巨大的整体事物,而是考虑一个 resize 只知道事物表面的设计,以及如何将工作委托给其他(更容易测试的)部分.

这是正常的,因为 TDD 主要是为了选择支持更好测试的设计。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM