0

I'm currently working on my first Laravel project — a service endpoint that returns a resource based on a recording saved in S3. The service doesn't require a DB, but my idea was, I could keep the controller skinny, by moving the logic to a "model". I could then access the resource by mimic'ing some standard active record calls.

Functionally, the implementation works as expected, but I am having issues with mocking.

I am using a library to create signed CloudFront URLs, but it is accessed as a static method. When I first started writing my feature test, I found that I was unable to stub the static method. I tried class aliasing with Mockery, but with no luck — I was still hitting the static method. So, I tried wrapping the static method in a little class assuming mocking the class would be easier. Unfortunately, I'm experiencing the same issue. The thing that I am trying to mock is being hit as if I'm not mocking it.

This stack overflow post gives an example of how to use class aliasing, but I can't get it to work. What is the difference between overload and alias in Mockery?

What am I doing wrong? I'd prefer to get mockery aliasing to work, but instance mocking would be fine. Please point me in the right direction.
 Thank you in advance for your help.

Controller

// app/Http/Controllers/API/V1/RecordingController.php
class RecordingController extends Controller {
    public function show($id){
        return json_encode(Recording::findOrFail($id));
    }
}

Model

// app/Models/Recording.php
namespace App\Models;

use Mockery;
use Carbon\Carbon;
use CloudFrontUrlSigner;
use Storage;
use Illuminate\Support\Arr;

class Recording
{
    public $id;
    public $url;

    private function __construct($array)
    {
        $this->id = $array['id'];
        $this->url = $this->signedURL($array['filename']);
    }

    // imitates the behavior of the findOrFail function
    public static function findOrFail($id): Recording
    {
        $filename = self::filenameFromId($id);
        if (!Storage::disk('s3')->exists($filename)) {
            abort(404, "Recording not found with id $id");
        }

        $array = [
            'id' => $id,
            'filename' => $filename,
        ];

        return new self($array);
    }

    // imitate the behavior of the find function
    public static function find($id): ?Recording
    {
        $filename = self::filenameFromId($id);
        if (!Storage::disk('s3')->exists($filename)){
            return null;
        }

        $array = [
            'id' => $id,
            'filename' => $filename,
        ];

        return new self($array);
    }

    protected function signedURL($key) : string
    {
        $url = Storage::url($key);
        $signedUrl = new cloudFrontSignedURL($url);
        return $signedUrl->getUrl($url);
    }
}

/**
 * wrapper for static method for testing purposes
 */
class cloudFrontSignedURL {
    protected $url;
    public function __construct($url) {
        $this->url = CloudFrontUrlSigner::sign($url);
    }
    public function getUrl($url) {
        return $this->url;
    }
}

Test

// tests/Feature/RecordingsTest.php
namespace Tests\Feature;

use Mockery;
use Faker;
use Tests\TestCase;
use Illuminate\Http\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Foundation\Testing\WithFaker;

/* The following is what my test looked like when I wrapped CloudFrontUrlSigner 
 * in a class and attempted to mock the class
 */
class RecordingsTest extends TestCase
{
    /** @test */
    public function if_a_recording_exists_with_provided_id_it_will_return_a_URL()
    {
        $recordingMock = \Mockery::mock(Recording::class);
        $faker = Faker\Factory::create();
        $id = $faker->numberBetween($min = 1000, $max = 9999);

        $filename = "$id.mp3";
        $path = '/api/v1/recordings/';
        $returnValue = 'abc.1234.com';

        $urlMock
            ->shouldReceive('getURL')
            ->once()
            ->andReturn($returnValue);

        $this->app->instance(Recording::class, $urlMock);

        Storage::fake('s3');
        Storage::disk('s3')->put($filename, 'this is an mp3');
        Storage::disk('s3')->exists($filename);

        $response = $this->call('GET', "$path$id");
        $response->assertStatus(200);
    }
}

// The following is what my test looked like when I was trying to alias CloudFrontUrlSigner
{
    /** @test */
    public function if_a_recording_exists_with_provided_id_it_will_return_a_URL1()
    {
        $urlMock = \Mockery::mock('alias:Dreamonkey\cloudFrontSignedURL');
        $faker = Faker\Factory::create();
        $id = $faker->numberBetween($min = 1000, $max = 9999);

        $filename = "$id.mp3";
        $path = '/api/v1/recordings/';
        $returnValue = 'abc.1234.com';

        $urlMock
            ->shouldReceive('sign')
            ->once()
            ->andReturn($returnValue);

        $this->app->instance('Dreamonkey\cloudFrontSignedURL', $urlMock);

        Storage::fake('s3');
        Storage::disk('s3')->put($filename, 'this is an mp3');
        Storage::disk('s3')->exists($filename);

        $response = $this->call('GET', "$path$id");
        $response->assertStatus(200);
    }
}

phpunit

$ phpunit tests/Feature/RecordingsTest.php --verbose

...

There was 1 failure:

1) Tests\Feature\RecordingsTest::if_a_recording_exists_with_provided_id_it_will_return_a_URL
Expected status code 200 but received 500.
Failed asserting that false is true.

/Users/stevereilly/Projects/media-service/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:133
/Users/stevereilly/Projects/media-service/tests/Feature/RecordingsTest.php:85
/Users/stevereilly/.composer/vendor/phpunit/phpunit/src/TextUI/Command.php:206
/Users/stevereilly/.composer/vendor/phpunit/phpunit/src/TextUI/Command.php:162

1 Answers1

0

You're getting a 500, which means there's something wrong with the code. Just by scanning it I notice you're missing a filenameFromId method on the Recordings class, and the Test is creating a mock named $recordingMock, but you try to use $urlMock. Try to fix those issues first.
Then you're mocking the class, but you never replace it in your application (you did it in the old test apparently).
Generally you want to follow these steps when mocking:
1. Mock a class
2. Tell Laravel to replace the class with your mock whenever someone requests it
3. Make some assertions against the mock

Borisu
  • 828
  • 7
  • 15
  • Thank you for your response. Not only was I referring to the wrong variable name ($recordingMock / $urlMock), The class I was mocking was incorrect -- 'alias:Dreamonkey\cloudFrontSignedURL' should have been 'alias:CloudFrontUrlSigner'. – steve_reilly Mar 07 '19 at 15:49