TDD:模拟对象堆栈的最佳做法

问题描述 投票:1回答:2

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

我的控制器取决于构造函数中提示的特定自定义接口类型。当然,我在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的实例(验证结果)。当调用Illuminate\Support\MessageBag时,这又返回errors()的实例。 MessageBag然后具有toArray()方法。

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

    /** @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对象堆栈?还是我的体系结构有问题,以至于感觉“设计过度”?

谢谢

php architecture phpunit tdd lumen
2个回答
1
投票

您正在做的不是单元测试,因为您不是在测试应用程序的单个单元。这是一个集成测试,是通过单元测试框架执行的,这就是直观上看起来错误的原因。

[单元测试和integration testing]发生在不同的时间,不同的位置,并且需要不同的方法和工具-前者测试代码的每个类和功能,而后者则不关心它们,它们只请求API和验证响应。另外,IT并不暗示要嘲笑任何东西,因为它的目的是测试您的单元之间的集成程度。

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

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

所有这些导致您只想单独测试CustomValidatorContract的想法。您甚至不需要在此模拟任何东西,只需实例化验证器,为它提供几组输入数据并检查其运行情况。


0
投票

这是可以正常运行的测试-但是有人可以告诉我是否有更好的编写方法吗?感觉不对。是否由于我在后台使用框架而需要大量的moc对象堆栈?还是我的体系结构有问题,以至于感觉“设计过度”?

大量的模拟对象表明您的测试主题与许多不同的事物紧密相关。

如果要支持更简单的测试,则需要使设计更简单。

换句话说,不是Controller.resize是一堆巨大的整体事物,它了解所有事物的所有细节,而是考虑一种设计,其中调整大小仅了解事物的表面,以及如何将工作委托给其他人(更容易测试) )件。

这很正常,从某种意义上说,TDD在选择支持更好测试的设计上很有用。

© www.soinside.com 2019 - 2024. All rights reserved.