如何使用 getMockBuilder() 为自定义服务实现 PHP UnitTest?

Posted

技术标签:

【中文标题】如何使用 getMockBuilder() 为自定义服务实现 PHP UnitTest?【英文标题】:How to implement PHP UnitTest using getMockBuilder() for a custom service? 【发布时间】:2021-09-09 09:47:10 【问题描述】:

我正在尝试在 Mezzio (Zend Expressive) 中为我的 AddHandler::class 编写 php 单元测试,但我不确定我做对了还是错了。尽管测试通过了,但我并不真正相信这是这样做的方法。要求基本上是模拟服务(new CrmApiService())->getUsers()(new CustomHydrator())->getHydrated($this->usersJson) 的输出,这可以保存在文本文件中。我还有另一个ViewHandler::class,它也使用数据服务进行列表,如果我得到这个线索,我相信我可以实现。

我的 AddHandler 类

namespace Note\Handler;

use App\Service\CrmApiService;
use App\Service\CustomHydrator;
use Laminas\Diactoros\Response\RedirectResponse;
use Mezzio\Flash\FlashMessageMiddleware;
use Mezzio\Flash\FlashMessagesInterface;
use Note\Form\NoteForm;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Laminas\Diactoros\Response\htmlResponse;
use Mezzio\Template\TemplateRendererInterface;

class AddHandler implements MiddlewareInterface

    /** @var NoteForm $noteForm */
    private $noteForm;
    /** @var TemplateRendererInterface $renderer */
    private $renderer;
    /** @var string $usersJson */
    private $usersJson;

    /**
     * AddHandler constructor.
     * @param NoteForm $noteForm
     * @param TemplateRendererInterface $renderer
     */
    public function __construct(NoteForm $noteForm, TemplateRendererInterface $renderer)
    
        $this->noteForm = $noteForm;
        $this->renderer = $renderer;
    

    /**
     * @param ServerRequestInterface $request
     * @param RequestHandlerInterface $handler
     * @return ResponseInterface
     */
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    
        $this->usersJson = (new CrmApiService())->getUsers();
        $hydratedUsers = (new CustomHydrator())->getHydrated($this->usersJson);

        $userArray = [];
        foreach ($hydratedUsers as $user) 
            $userArray[] = $user;
        
        $userSelectValueOptions = [];
        foreach ($userArray as $key => $val) 
            $userSelectValueOptions[$val["personReference"]] = $val["givenName"] . " " . $val["additionalName"] . " " . $val["familyName"];
        

        if ($request->getMethod() === "POST") 
            $this->noteForm->setData(
                $request->withoutAttribute("saveNote")->withoutAttribute("referrerId")->getParsedBody()
            );

            // NB: assignedUserID received by form submission is assigned a dummy User Name and is then
            // appended at the end of formSelect("assignedUserID") for noteForm validation in below code block
            $userSelectValueOptions[$this->noteForm->get("assignedUserID")->getValue()] = "Testing User";
            $userSelect = $this->noteForm->get("assignedUserID");
            $userSelect->setValueOptions($userSelectValueOptions);
            //todo: remove the above code block before production

            $referrerId = $request->getAttribute("referrerId");
            $parent = $request->getAttribute("parent");
            $parentID = $request->getAttribute("parentID");

            if ($this->noteForm->isValid()) 
                (new CrmApiService())->createNote($this->noteForm->getData());
                $successMessage = "Note successfully added.";

                $response = $handler->handle($request);

                /** @var FlashMessagesInterface $flashMessages */
                $flashMessages = $request->getAttribute(FlashMessageMiddleware::FLASH_ATTRIBUTE);

                if ($response->getStatusCode() !== 302) 
                    $flashMessages->flash("success", $successMessage);
                    return new RedirectResponse(
                        (substr(
                            $referrerId,
                            0,
                            3
                        ) == "brk" ? "/broker/" : "/enquiry/") . $referrerId . "/" . $parent . "/" . $parentID
                    );
                
                return $response;
            
        

        $referrerId = $request->getAttribute("referrerId");
        $parentID = $request->getAttribute("parentID");
        $parent = $request->getAttribute("parent");

        $userSelect = $this->noteForm->get("assignedUserID");
        $userSelect->setValueOptions($userSelectValueOptions);

        $noteParent = $this->noteForm->get("parent");
        $noteParent->setValue($parent);
        $noteParentID = $this->noteForm->get("parentID");
        $noteParentID->setValue($parentID);

        return new HtmlResponse(
            $this->renderer->render(
                "note::edit",
                [
                    "form" => $this->noteForm,
                    "parent" => $parent,
                    "parentID" => $parentID,
                    "referrerId" => $referrerId
                ]
            )
        );
    

PHP 单元测试

declare(strict_types=1);

namespace NoteTests\Handler;

use Note\Handler\AddHandler;
use Mezzio\Template\TemplateRendererInterface;
use Note\Form\NoteForm;
use Note\Handler\EditHandler;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class NoteAddEditHandlerTest extends TestCase

    use ProphecyTrait;

    /** @var NoteForm */
    private $noteForm;
    /** @var TemplateRendererInterface */
    private $renderer;

    public function testRendersAddFormProperly()
    
        $this->renderer
            ->render("note::edit", Argument::type("array"))
            ->willReturn(true);

        $serverRequest = $this->createMock(ServerRequestInterface::class);
        $requestHandler = $this->createMock(RequestHandlerInterface::class);

        $mock = $this->getMockBuilder(AddHandler::class)
            ->onlyMethods(["process"])
            ->setConstructorArgs([$this->noteForm, $this->renderer->reveal()])
            ->getMock();

        $mock->expects($this->once())
            ->method("process")
            ->with($serverRequest, $requestHandler);

        $mock->process($serverRequest, $requestHandler);
    

    /**
     *
     */
    protected function setUp(): void
    
        $this->noteForm = new NoteForm();
        $this->renderer = $this->prophesize(TemplateRendererInterface::class);
    


编辑(期望的结果)

AddHandler->process() 方法呈现一个页面,这是我希望看到的 UnitTest 也针对响应进行测试,但我不确定如何测试它。我认为这个代码块的末尾应该有一些返回值will()

$mock->expects($this->once())
            ->method("process")
            ->with($serverRequest, $requestHandler);

【问题讨论】:

【参考方案1】:

虽然测试通过了,但我并不真正相信这是这样做的方法。

如果您已经编写了该测试并且这是您的判断,我建议您暂时重写测试(例如,在另一个测试方法中)您在测试中测试您的期望以验证它们是否得到解决。

否则,测试似乎对您没有好处,因为您不了解测试的目的,因此是多余的代码和浪费(在敏捷意义上),您可以干净利落地删除它,而不是让它潜伏在那里躺着。

谁需要一个测试内容不清楚的测试?特别是在单元测试中,测试失败的原因应该只有一个。不明确的测试是不可能的。

是否已经到了清理时间然后回到绘图板?可能是。我建议首先进行增量改进和一些沙盒。就像添加一个大大简化的测试用例方法来验证您自己对测试套件框架和(两个?)正在使用的模拟库的期望。

这也将帮助您开始使用正在使用的框架并获得更深入的了解 - 这通常会立即获得回报。

我还有一个 ViewHandler::class,它也使用一个服务来列出数据,如果我得到这个的线索,我相信我可以实现它。

你的代码你的测试。只有您能说出您的测试是否满足您的要求。

如果您允许我发表个人评论,我讨厌在测试中进行嘲笑。即使对于代码模拟在技术上有效,它很快就会变得很麻烦,并且倾向于测试只测试为测试而编写的模拟,完全是不必要的工作。

相反,我尝试直接让被测代码直接进行测试,如果某个抽象需要预先进行大量设置,请为其创建一个工厂,然后该工厂也可以用于测试,从而将开销减少到最低限度。

然后可以对工厂进行一些专门的测试,自动注入测试配置(例如,如果必须将测试不应该进入的其他系统设置为空白,则以模拟的形式)然后让它通过。但这只是示例。

在您喜欢测试system($request, $response)->assert(diverse on $response afterwards) 的系统中,其中system 是您编写的具体类的*(您的实现),您可能希望有一个* 的测试器,以便您的测试过程保持不变清楚system 提供和* 实现的所有接口,您不需要为* 设置所有system 的内部结构,仅用于测试任何*,例如HandlerTester

如果处理程序需要更高级别的抽象实现,还要检查 Mezzio 本身是否不提供测试器。一个好的库通常会附带好的测试工具(即使在这种情况下没有,你也可以随时 fork)。

测试应该在开发之前进行,这对于库来说是如此,所以实际上我个人希望这些东西已经存在于 0.0.1 中。但这可能会有所不同。

还为您的测试启用代码覆盖率,这样您就可以更轻松地检查您的测试是否按预期方式运行,并将所有协作者置于测试和覆盖范围内。这有助于更好地了解测试的作用,并且可能已经阐明了它是否有用。

【讨论】:

我已经更新了我原来的问题。我需要根据某种响应对其进行测试。 我已更改代码以期望得到响应 $mock->method("process")->with($serverRequest, $requestHandler)->willReturn($responseInterface);,然后断言为 $response = $mock->process($serverRequest, $requestHandler); $this->assertSame($response, $responseInterface);,其中 $responseInterfaceResponseInterface::class 的模拟。这样对吗?不过测试确实运行成功。 你为什么要模拟你想测试的主题?例如,为什么您不能只创建处理程序而只模拟协作者? (这是一个关于你自己理解的问题,而不是对错,以防万一问题在网上很容易被误读) 因为处理程序需要在运行时填充表单元素(选择),这再次需要处理程序工厂中的container 来初始化表单,它会打开一个潘多拉盒子的依赖关系。例如在AddHandlerFactory::class 中,我将表单初始化为return new AddHandler($container->get("FormElementManager")->get(NoteForm::class), $container->get(TemplateRendererInterface::class));。当我跟随你的领导时,它给了我错误,Laminas\Form\Exception\InvalidElementException: No element by the name of [parent] found in form @GoharSahi:有没有可能仅通过处理程序的构造函数注入?据我记得(但不是 Mezzio 的足够熟练的用户)Mezzio 明确了所有依赖项,不应该有任何隐藏的依赖项。【参考方案2】:

这是我的解决方案。我将 ResponseInterface::class 模拟为 $this->responseInterface 并使用 process 方法返回它。

public function testRendersEditFormProperly()
    
        $this->renderer
            ->render("note::edit", Argument::type("array"))
            ->willReturn(true);

        $mock = $this->getMockBuilder(EditHandler::class)
            ->onlyMethods(["process"])
            ->setConstructorArgs([$this->noteForm, $this->renderer->reveal()])
            ->getMock();

        $mock->method("process")
            ->with($this->serverRequest, $this->requestHandler)
            ->willReturn($this->responseInterface);

        $response = $mock->process($this->serverRequest, $this->requestHandler);
        $this->assertSame($response, $this->responseInterface);
    

【讨论】:

以上是关于如何使用 getMockBuilder() 为自定义服务实现 PHP UnitTest?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 WCF 自定义行为中动态更改 URL

word2016如何自定义安装

如何将自定义子视图的宽度设置为没有宽度限制的父 UIView 的 1/3?

如何在 Spring Boot 应用程序中为码头服务器自定义带有自定义正文内容的 ErrorHandler 类

为访问查询创建自定义聚合函数

如何在Active Admin中为自定义生成的页面添加导出为csv选项