TDD:模拟对象堆栈的最佳实践 [英] TDD: best practices mocking stacks of objects

查看:121
本文介绍了TDD:模拟对象堆栈的最佳实践的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm trying to get familiar with unit testing in PHP with a small API in Lumen. Writing the first few tests was pretty nice with the help of some tutorials but now I encountered a point where I have to mock/ stub a dependency.

我的控制器取决于构造函数中提示的特定自定义接口类型.
当然,我在ServiceProvider中定义了此接口/实现绑定.

My controller depends on a specific custom interface type hinted in the constructor.
Of course, I defined this interface/implementation-binding within a 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()方法.

As you can see, my CustomValidatorContract has a method validate() which returns an instance of Illuminate\Contracts\Validation\Validator (the validation result). This in turn returns an instance of Illuminate\Support\MessageBag when errors() is called. MessageBag then has a toArray()-method.

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

Now I want to test the behavior of my controller in case the validation fails.

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

This is a test that runs ok - but can someone please tell me if there is a better way to write this? It doesn't feel right. Is this big stack of moc-objects needed because of the fact that I'm using a framework in the background? Or is there something wrong with my architecture so that this feels so "overengineered"?

谢谢

推荐答案

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

What you are doing is not unit testing because you are not testing a single unit of your application. This is an integration test, performed with unit testing framework, and this is the reason it looks intuitively wrong.

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

Unit testing and integration testing happen at different times, at different places and require different approaches and tools - the former tests every single class and function of your code, while latter couldn't care less about those, they just request APIs and validate responses. Also, IT doesn't imply mocking anything because it's goal is to test how well your units integrate with each other.

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

You'll have hard time supporting tests like that because every time you change CustomValidatorContract you'll have to fix all the tests involving it. This is how UT improves code design by requiring it to be as loosely coupled as possible (so you could pick a single unit and use it without the need to boot entire app), respecting SRP & OCP, etc.

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

You don't need to test 3rd party code, pick an already tested one instead. You don't need to test side effects either, because environment is just like 3rd party service, it should be tested separately (return response() is a side effect). Also it seriously slows down the testing.

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

All that leads to the idea that you only want to test your CustomValidatorContract in isolation. You don't even need to mock anything there, just instantiate the validator, give it few sets of input data and check how it goes.

这篇关于TDD:模拟对象堆栈的最佳实践的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆