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

How to implement PHP UnitTest using getMockBuilder() for a custom service?

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

我的 AddHandler Class

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 也针对响应进行测试,但我不确定如何测试它。我认为在此代码块的末尾应该有一些 return 值 will()

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

Although the Test passes but I'm not really convinced that's the way to do it.

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

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

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

现在是清理时间然后回到绘图板吗?可能是。我个人建议首先进行渐进式改进和一些沙盒。就像添加一个大大减少的测试用例方法来验证您自己对测试套件框架的期望和(两个?)正在使用的模拟 library/ies。

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

I've another one ViewHandler::class which also uses a service for data for listing, which I'm sure I can implement if I get a clue for this one.

你的代码你的测试。只有您可以说您的测试是否完全满足您的要求。

如果你允许我发表个人评论,我讨厌在测试中进行模拟。即使代码模拟在技术上可行,它很快就会变得很麻烦,并且有一种趋势,即测试只测试为测试编写的模拟,因此完全是不必要的工作。

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

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

在您想要测试 system($request, $response)->assert(diverse on $response afterwards) 的系统中,其中 system* 您编写的具体 类(您的实现),您可能需要一个* 的测试器,以便您的测试程序在 system 提供和 * 实现的所有接口上保持清晰,并且您不需要设置所有 [=11] 的内部结构=] 用于 * 仅用于测试任何 *,例如HandlerTester.

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

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

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

这是我的解决方案。我已经将 ResponseInterface::class 嘲笑为 $this->responseInterface 并将 process 方法设为 return this.

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);
    }