0

I'm trying to write a PHP UnitTest for my AddHandler::class in Mezzio (Zend Expressive) but I'm not sure I've done it right or wrong. Although the Test passes but I'm not really convinced that's the way to do it. The requirement is to basically mock the output of service (new CrmApiService())->getUsers() and (new CustomHydrator())->getHydrated($this->usersJson) which can be saved in a text file for that matter. 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.

My 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 UnitTest

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

}

Edit (Desired Result)

The AddHandler->process() method renders a page and this is what I'd like to see that the UnitTest also test against a response but I'm not sure how to test that. I think there should be some return value at the end of this code block with will()

$mock->expects($this->once())
            ->method("process")
            ->with($serverRequest, $requestHandler);
GoharSahi
  • 488
  • 1
  • 8
  • 22

2 Answers2

0

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

If you've written that test and this is your judgement, I suggest you temporarily rewrite the test (e.g. in another test-method) where you test for your expectations in testing to verify they are addressed.

Otherwise it seems the test is not of your benefit as you don't understand what it tests for and therefore is superfluous code and waste (in the agile sense) and you can cleanly just remove it and not let it lurk there open to lying around.

Who needs a test that is unclear in what it tests? Especially in unit tests there should be only one reason why a test fails. Not possible with an unclear test.

Is it already cleanup time and then back to drawing board? Maybe. I'd suggest incremental improvement and some sandboxing personally first. Like adding a much reduced test-case-method for verifying your own expectation of the test-suite-framework and the (two?) mocking library/ies in use.

This will also help you get going with the framework in use and gain a deeper understanding - this normally immediately pays off.

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.

Your code your tests. Only you can say whether or not your tests full-fill your requirements.

And if you allow me a personal comment, I hate to do mocking in tests. Even for code mocking technically works it becomes cumbersome pretty soon and has the tendency that the tests are only testing the mocks that have been written for the test only so entirely needless work.

Instead I try to either have the code under test straight forward and if a certain abstraction requires a lot of set-up upfront, create a factory for it and then that factory can be used in tests, too, reducing the overhead to a minimum.

Then some specialization of the factory can be done for testing automatically inject the testing configuration (e.g. in form of mocks if it must be to set blank other systems the test should not reach into) and then just let it pass. But this is just exemplary.

In a system where you like to test system($request, $response)->assert(diverse on $response afterwards) where system is * of concrete classes you write (your implementation), you may want to have a tester for * so that your test-procedure remains clear on all the interfacing that system offers and * implements and you don't need to set-up internals of all of system for * only to test any *, e.g. a HandlerTester.

Also check if Mezzio itself is not offering a tester if there is a higher level abstraction implementation necessary for handlers. A good library normally ships with good testing utilities (and even in this case not, you can fork it anytime).

Testing should be before development, this is oh so true for libraries, so actually I would personally expect the stuff is already there in 0.0.1. But this can vary.

Enable code coverage also for your tests so you can more easily review if your tests do run the way it's intended and put also all collaborators under test and coverage. This can help to gain more understanding what a test does and maybe already clarifies if it is of use or not.

hakre
  • 193,403
  • 52
  • 435
  • 836
  • I've updated my original question. I need to test it against some kind of response. – GoharSahi Jun 26 '21 at 11:18
  • I've changed the code to expect a response ```$mock->method("process")->with($serverRequest, $requestHandler)->willReturn($responseInterface);``` and then asserted as ```$response = $mock->process($serverRequest, $requestHandler); $this->assertSame($response, $responseInterface);``` where `$responseInterface` is the mock for `ResponseInterface::class`. Is it right? The test does run successfully though. – GoharSahi Jun 26 '21 at 12:24
  • Why do you mock the subject you want to test? Why can't you just create the handler and only mock collaborators for example? (this is a question about your own understanding, not whether this would be wrong or right, just in case as questions can be easily misread online) – hakre Jun 26 '21 at 14:04
  • Because the handler requires a form element (select) to be populated on runtime which again requires the `container` in handler factory for initialization of form and it'll open up a pandora box of dependencies. e.g. in `AddHandlerFactory::class` I've initialized the form as ```return new AddHandler($container->get("FormElementManager")->get(NoteForm::class), $container->get(TemplateRendererInterface::class));```. When I followed your lead, it gave me the error, `Laminas\Form\Exception\InvalidElementException: No element by the name of [parent] found in form` – GoharSahi Jun 27 '21 at 04:58
  • @GoharSahi: Any chance this can be injected just via the constructor of the handler? From what I remember (but not a proficient enough user of Mezzio) Mezzio made all dependencies explicit and should not have any more hidden ones. – hakre Jun 27 '21 at 05:05
  • But true, I see, the container needs its own setup. But still, it should be possible to inject it. You can mock your handler, but I would try to at least spare that mock so that you don't have to create one for every test of every handler you write. – hakre Jun 27 '21 at 05:07
  • I did give it a try with mocking `container` but it seems to be not working when mocked. `Error: Call to undefined method Mock_Container_0561c534::get()` and with mocking `ContainerInterface::class` (just to ensure), it gives the error `Error: Call to a member function get() on null` – GoharSahi Jun 27 '21 at 05:43
  • Well, I would not have mocked the container, but just use a new one and fill it with mocks. Mezzio should ship with its own container implementation. – hakre Jun 27 '21 at 05:58
0

Here's my solution. I've mocked ResponseInterface::class as $this->responseInterface and make the process method to 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);
    }
GoharSahi
  • 488
  • 1
  • 8
  • 22