Skip to content

Unit Tests

Einar Valur Aðalsteinsson edited this page Jun 27, 2015 · 16 revisions

Just by pulling all dependencies down with composer, you can unit test the while system.

Test all

Navigate to the tests directory and run phpunit from there

cd Althingi/model/Althingi/tests
../vendor/bin/phpunit [enter]

Now open Althingi/module/Althingi/docs/tests/report/index.html to see coverage.

Test each class.

When you are creating unit-tests it is good to run test on one class. For this: configure PHPStorm like so: screen shot 2015-06-27 at 2 38 54 pm

Now you can right-click on a TestCase and run it in PHPStorm screen shot 2015-06-27 at 2 48 16 pm

See how wonderful that green line is screen shot 2015-06-27 at 2 44 05 pm

Setting PHPStorm up to run your unit tests means that you can use breakpoints in your unit-tests screen shot 2015-06-27 at 2 45 57 pm

Test Controllers.

To set up a Controller test case. start off with this:

namespace Althingi\Controller;

use Zend\Test\PHPUnit\Controller\AbstractHttpControllerTestCase;

class SomeControllerTest extends AbstractHttpControllerTestCase
{
    public function setUp()
    {
        $this->setApplicationConfig(
            include __DIR__ .'/../../../../config/application.config.php'
        );
        parent::setUp();
    }
}

Make sure that setUp is always the top-most method defined. It tells a new developer that all tests below are dependent on this config file.

There are few things that we need to test in regards to controllers. First we have to make sure that they are calling correct Controllers and actions. The router is just and array of arrays and it can become quite complicated over time. It is really hard to monitor if you are not overwriting something, once you get into child and child-of-child-of-child routes.

public function testGetSuccess()
{
    $this->dispatch('/api/loggjafarthing/100/thingmal/200', 'GET');

    $this->assertControllerClass('IssueController');
    $this->assertActionName('get');
    $this->assertResponseStatusCode(200);
}

This also makes sure that the status code is 200.

Mocking the Service

Lets take a better look at the method that this tests case is testing.

class IssueController extends AbstractRestfulController
{
    public function get($id)
    {
        $assemblyId = $this->params('id', 0);
        $issue = $this->getServiceLocator()->get('Althingi\Service\Issue')
            ->get($id, $assemblyId);

        if (!$issue) {
            return $this->notFoundAction();
        }

        return new ItemModel($issue);
    }
}

What is it doing? It is fetches a service object, it calls get on that, if the Service returns nothing, return 404, else return 200. At this point we are not interested in testing the Service. That is another unit and deserves another unit-test. We are interested in this if statement. So we will mock the service. To do this, we use a mock framework called Mockery. A mock service allows us to fake a class and the methods on it.

To mock a class you pass in the name of the class to the mock constructor.

To mock a method, you call shouldReceive('nameOfMethod').

To mock what a method returns, you call andReturn($returnValue).

To check how often that method is called, you call never(), once(), twice() or times(int).

At the end, if you are chaining the creation process, you call getMock().

Successful GET.

Because we don't new our Service in our controller, but dependency inject it. (This could be a good place to go into why we never call the new operator in our application, but I'm not going to. Just know that it has to do with testing and Dependency Injection), we can now inject our mock service into the controller. It will then not use the Service that goes to the database but a mock one that does nothing.

public function testGet()
{
    $serviceMock = \Mockery::mock('Althingi\Service\Issue')
        ->shouldReceive('get')
            ->andReturn(new \stdClass())
            ->once()
            ->getMock();

    $serviceManager = $this->getApplicationServiceLocator();
    $serviceManager->setAllowOverride(true);
    $serviceManager->setService('Althingi\Service\Issue', $serviceMock);

    $this->dispatch('/api/loggjafarthing/100/thingmal/4', 'GET');
    $this->assertControllerClass('IssueController');
    $this->assertActionName('get');
    $this->assertResponseStatusCode(200);
}

Unsuccessful GET.

Next up is to test that if no resource is found that 404 is returned. The test is almost the same, The difference is that andReturn(null) returns null and the status-code is 404

public function testGet()
{
    $serviceMock = \Mockery::mock('Althingi\Service\Issue')
        ->shouldReceive('get')
            ->andReturn(null)
            ->once()
            ->getMock();

    $serviceManager = $this->getApplicationServiceLocator();
    $serviceManager->setAllowOverride(true);
    $serviceManager->setService('Althingi\Service\Issue', $serviceMock);

    $this->dispatch('/api/loggjafarthing/100/thingmal/4', 'GET');
    $this->assertControllerClass('IssueController');
    $this->assertActionName('get');
    $this->assertResponseStatusCode(404);
}

It is important here that you still call once() making sure that the service is still being called.

Update.

Now for a more complicated test (not really). Lets test an update action. Here is the action we want to test:

public function patch($id, $data)
{
    $assemblyId = $this->params('id', 0);
    $issueService = $this->getServiceLocator()->get('Althingi\Service\Issue');
    $issue = $issueService->get($id, $assemblyId);

    if (!$issue) {
        return $this->notFoundAction();
    }

    $form = new Issue();
    $form->setObject($issue);
    $form->setData($data);

    if ($form->isValid()) {
        $issueService->update($form->getObject());
        return (new EmptyModel())->setStatus(200);
    }

    return (new ErrorModel($form))->setStatus(400);
}

We are still testing HOW the service operates inside the Controller. So for successful operation we want the get($id, $assemblyId) to be called once and we want update($form->getObject()) to be called once. If something goes wrong we still want our get($id, $assemblyId) method to be called once, but update($form->getObject()) never.

public function testPatch()
{
    $serviceMock = \Mockery::mock('Althingi\Service\Issue')
        ->shouldReceive('get')
            ->andReturn(new \stdClass())
            ->once()
            ->getMock()
        ->shouldReceive('update')
            ->once()
            ->andReturn(null)
            ->getMock();

    $serviceManager = $this->getApplicationServiceLocator();
    $serviceManager->setAllowOverride(true);
    $serviceManager->setService('Althingi\Service\Issue', $serviceMock);

    $this->dispatch('/api/loggjafarthing/100/thingmal/200', 'PATCH', [
        'name' => 'n1'
    ]);
    $this->assertControllerClass('IssueController');
    $this->assertActionName('patch');
    $this->assertResponseStatusCode(200);
}

See how I'm spying on how the Service is working inside the Controller by mocking it and inspect how the Controller interacts with it, mainly by the once() method.

Lets test if the resource is not found. For this we want our service->get to return null and we don't want the service to be calling update. Also we want to make sure that a 404 is returned.

public function testPatch()
{
    $serviceMock = \Mockery::mock('Althingi\Service\Issue')
        ->shouldReceive('get')
            ->andReturn(null)
            ->once()
            ->getMock()
        ->shouldReceive('update')
            ->never()
            ->andReturn(null)
            ->getMock();

    $serviceManager = $this->getApplicationServiceLocator();
    $serviceManager->setAllowOverride(true);
    $serviceManager->setService('Althingi\Service\Issue', $serviceMock);

    $this->dispatch('/api/loggjafarthing/100/thingmal/200', 'PATCH', [
        'name' => 'n1'
    ]);
    $this->assertControllerClass('IssueController');
    $this->assertActionName('patch');
    $this->assertResponseStatusCode(404);
}

Going full 007 on our service.

I really want to know that is being passed into the service once it has been feed the object from the Form. Now we are venturing into a grey area here. This is not really the responsibility of the Controller. The Controller expects the Form to be tested somewhere else and should trust it to hand over a valid object. But for the sake of sheer curiosity, lets look inside Service just before it updates a record.

Mockery has andReturn() which we have used a lot. It also has a andReturnUsing(closure) which takes in a lambda function that is passed in the argument that the method should otherwise receive. Inside of that lambda function we can inspect that the Controller is passing into the Service.

First off, we create a private function that will return the same kind of data that the Database would return. This is what our service->get() function will return. Next we write the test and inspect what is getting passed into the update method with a series of assert calls.

class IssueControllerTest extends AbstractHttpControllerTestCase
{
    private function buildIssueObject()
    {
        return (object) [
            "issue_id"  => 2,
            "assembly_id"  => 144,
            "category"  => "A",
            "name"  => "Virðisaukaskattur o.fl.",
            "type"  => 1,
            "type_name"  => "Frumvarp til laga",
            "type_subname"  => "lagafrumvarp",
            "status" => "Samþykkt sem lög frá Alþingi.",
            "foreman" => (object) [
                'congressman_id' => 200
            ],
            "speakers" => []
        ];
    }

    public function testPatch()
    {
        $serviceMock = \Mockery::mock('Althingi\Service\Issue')
            ->shouldReceive('get')
                ->andReturn($this->buildIssueObject())
                ->once()
                ->getMock()
            ->shouldReceive('update')
            ->once()
            ->andReturnUsing(function ($object) {
                $this->assertObjectNotHasAttribute('foreman', $object);
                $this->assertObjectNotHasAttribute('speakers', $object);
                $this->assertObjectHasAttribute('congressman_id', $object);
                $this->assertEquals(200, $object->congressman_id);
                $this->assertEquals('n1', $object->name);
                $this->assertEquals('A', $object->category);
            })->getMock();

        $serviceManager = $this->getApplicationServiceLocator();
        $serviceManager->setAllowOverride(true);
        $serviceManager->setService('Althingi\Service\Issue', $serviceMock);

        $this->dispatch('/api/loggjafarthing/100/thingmal/200', 'PATCH', [
            'name' => 'n1'
        ]);
        $this->assertControllerClass('IssueController');
        $this->assertActionName('patch');
        $this->assertResponseStatusCode(200);
    }

See how this test is checking to see how the foreman and speakers are no longer there and how now there is a congressman_id property. This tests also that the name property got changed from Virðisaukaskattur o.fl. to n1 but category did not get changed.

Clone this wiki locally