简体   繁体   English

TDD:最佳实践 mocking 对象堆栈

[英]TDD: best practices mocking stacks of objects

I'm trying to get familiar with unit testing in PHP with a small API in Lumen.我正在尝试熟悉 PHP 中的单元测试和 Lumen 中的小 API。 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.在一些教程的帮助下编写前几个测试非常好,但现在我遇到了必须模拟/存根依赖项的问题。

My controller depends on a specific custom interface type hinted in the constructor.我的 controller 取决于构造函数中提示的特定自定义接口类型。
Of course, I defined this interface/implementation-binding within a ServiceProvider.当然,我在 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']);
        }
    }

As you can see, my CustomValidatorContract has a method validate() which returns an instance of Illuminate\Contracts\Validation\Validator (the validation result).如您所见,我的CustomValidatorContract有一个validate()方法,它返回一个Illuminate\Contracts\Validation\Validator实例(验证结果)。 This in turn returns an instance of Illuminate\Support\MessageBag when errors() is called.当调用errors()时,这又会返回Illuminate\Support\MessageBag的实例。 MessageBag then has a toArray() -method. MessageBag然后有一个toArray()方法。

Now I want to test the behavior of my controller in case the validation fails.现在我想测试我的 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);
    }

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?由于我在后台使用框架,是否需要这么大堆 moc 对象? Or is there something wrong with my architecture so that this feels so "overengineered"?还是我的架构有问题,以至于感觉如此“过度设计”?

Thanks谢谢

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.这是一个集成测试,使用单元测试框架执行,这就是它看起来直观错误的原因。

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.单元测试和集成测试发生在不同的时间、不同的地方,并且需要不同的方法和工具——前者测试代码的每一个 class 和 function,而后者并不关心这些,他们只是请求 API 并验证响应。 Also, IT doesn't imply mocking anything because it's goal is to test how well your units integrate with each other.此外,IT 并不意味着 mocking 任何事情,因为它的目标是测试您的单元之间的集成程度。

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

You don't need to test 3rd party code, pick an already tested one instead.您不需要测试第 3 方代码,而是选择一个已经测试过的代码。 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).您也不需要测试副作用,因为环境就像 3rd 方服务,应该单独测试( return response()是副作用)。 Also it seriously slows down the testing.它还严重减慢了测试速度。

All that leads to the idea that you only want to test your CustomValidatorContract in isolation.所有这些导致您只想单独测试您的CustomValidatorContract的想法。 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.你甚至不需要在那里模拟任何东西,只需实例化验证器,给它几组输入数据并检查它是如何进行的。

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?由于我在后台使用框架,是否需要这么大堆 moc 对象? Or is there something wrong with my architecture so that this feels so "overengineered"?还是我的架构有问题,以至于感觉如此“过度设计”?

The big stack of mock objects indicates that your test subject is tightly coupled to many different things.大量的模拟对象表明您的测试对象与许多不同的事物紧密耦合。

If you want to support simpler tests, then you need to make the design simpler.如果您想支持更简单的测试,那么您需要使设计更简单。

In other words, instead of Controller.resize being one enormous monolithic thing that knows all of the details about everything, think about a design where resize only knows about the surface of things, and how to delegate work to other (more easily tested) pieces.换句话说,不是Controller.resize是一个了解所有细节的巨大的整体事物,而是考虑一个 resize 只知道事物表面的设计,以及如何将工作委托给其他(更容易测试的)部分.

This is normal, in the sense that TDD is a lot about choosing designs that support better testing.这是正常的,因为 TDD 主要是为了选择支持更好测试的设计。

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

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