2
votes

I am new to unit testing and trying to test a controller method in Laravel 5.1 and Mockery.

I am trying to test a registerEmail method I wrote, below:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Response;
use Mailchimp;
use Validator;

/**
 * Class ApiController
 * @package App\Http\Controllers
 */
class ApiController extends Controller
{

    protected $mailchimpListId = null;
    protected $mailchimp = null;

    public function __construct(Mailchimp $mailchimp)
    {
        $this->mailchimp = $mailchimp;
        $this->mailchimpListId = env('MAILCHIMP_LIST_ID');
    }

    /**
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function registerEmail(Request $request)
    {

        $this->validate($request, [
            'email' => 'required|email',
        ]);

        $email  = $request->get('email');

        try {
            $subscribed = $this->mailchimp->lists->subscribe($this->mailchimpListId, [ 'email' => $email ]);
            //var_dump($subscribed);
        } catch (\Mailchimp_List_AlreadySubscribed $e) {
            return Response::json([ 'mailchimpListAlreadySubscribed' => $e->getMessage() ], 422);
        } catch (\Mailchimp_Error $e) {
            return Response::json([ 'mailchimpError' => $e->getMessage() ], 422);
        }

        return Response::json([ 'success' => true ]);
    }
}

I am attempting to mock the Mailchimp object to work in this situation. So far, my test looks as follows:

<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class HomeRouteTest extends TestCase
{

    use WithoutMiddleware;

    public function testMailchimpReturnsDuplicate() {
        $listMock = Mockery::mock('Mailchimp_Lists')
            ->shouldReceive('subscribe')
            ->once()
            ->andThrow(\Mailchimp_List_AlreadySubscribed::class);

        $mailchimp = Mockery::mock('Mailchimp')->lists = $listMock;

        $this->post('/api/register-email', ['email'=>'[email protected]'])->assertJson(
            '{"mailchimpListAlreadySubscribed": "[email protected] is already subscribed to the list."}'
        );
    }
}

I have phpUnit returning a failed test.

HomeRouteTest::testMailchimpReturnsDuplicate Mockery\Exception\InvalidCountException: Method subscribe() from Mockery_0_Mailchimp_Lists should be called exactly 1 times but called 0 times.

Also, if I assert the status code is 422, phpUnit reports it is receiving a status code 200.

It works fine when I test it manually, but I imagine I am overlooking something fairly easy.

2
Hi. What is your full path to Mailchimp class? Not alias but full path with namespace.Armen Markossyan
I'm using the skovmand/mailchimp-laravel package here: github.com/skovmand/mailchimp-laravel. The Mailchimp class is in the global namespace, and require_once's a lot of other classes in the file as well.Lea
You are mocking Mailchimp_Lists, but i don't see that class being loaded anywhere in your controller.PickYourPoison
It's not included in the controller, the Mailchimp class file require_onces Mailchimp_Lists in it. I think by mocking Mailchimp and setting the lists parameter on it to be a mock, that should work around the problem? Obviously something's still wrong with it.Lea
Maybe an expansion on the question: how would you test this?Lea

2 Answers

1
votes

I managed to solve it myself. I eventually moved the subscribe into a seperate Job class, and was able to test that be redefining the Mailchimp class in the test file.

class Mailchimp {
    public $lists;

    public function __construct($lists) {
        $this->lists = $lists;
    }
}

class Mailchimp_List_AlreadySubscribed extends Exception {}

And one test

public function testSubscribeToMailchimp() {
    // create job
    $subscriber = factory(App\Models\Subscriber::class)->create();
    $job = new App\Jobs\SubscribeToList($subscriber);

    // set up Mailchimp mock
    $lists = Mockery::mock()
        ->shouldReceive('subscribe')
        ->once()
        ->andReturn(true)
        ->getMock();

    $mailchimp = new Mailchimp($lists);

    // handle job
    $job->handle($mailchimp);

    // subscriber should be marked subscribed
    $this->assertTrue($subscriber->subscribed);
}
0
votes

Mockery will expect the class being passed in to the controller be a mock object as you can see here in their docs:

class Temperature
{
    public function __construct($service)
    {
        $this->_service = $service;
    }
}

Unit Test

$service = m::mock('service');
$service->shouldReceive('readTemp')->times(3)->andReturn(10, 12, 14);

$temperature = new Temperature($service);

In laravel IoC it autoloads the classes and injects them, but since its not autoloading Mailchimp_Lists class it won't be a mock object. Mailchimp is requiring the class atop it's main class require_once 'Mailchimp/Lists.php';

Then Mailchimp is then loading the class automatically in the constructor

$this->lists = new Mailchimp_Lists($this);

I don't think you'll be able to mock that class very easily out of the box. Since there isn't away to pass in the mock object to Mailchimp class and have it replace the instance of the real Mailchimp_Lists

I see you are trying to overwrite the lists member variable with a new Mock before you call the controller. Are you certain that the lists object is being replaced with you mocked one? Try seeing what the classes are in the controller when it gets loaded and see if it is in fact getting overridden.