【问题标题】:Mocking class instantiation in a feature test在功能测试中模拟类实例化
【发布时间】:2019-07-27 06:02:50
【问题描述】:

我目前正在处理我的第一个 Laravel 项目——一个服务端点,它根据保存在 S3 中的记录返回资源。该服务不需要数据库,但我的想法是,我可以通过将逻辑移动到“模型”来保持控制器的瘦身。然后我可以通过模仿一些标准的活动记录调用来访问资源。

从功能上讲,实现按预期工作,但我遇到了模拟问题。

我正在使用库来创建签名的 CloudFront URL,但它是作为静态方法访问的。当我第一次开始编写我的功能测试时,我发现我无法对静态方法进行存根。我用Mockery 尝试了类别名,但没有运气——我仍然在使用静态方法。因此,我尝试将静态方法包装在一个小类中,假设模拟该类会更容易。不幸的是,我遇到了同样的问题。我试图嘲笑的东西被击中,就好像我不是在嘲笑它一样。

这篇堆栈溢出帖子给出了一个如何使用类别名的示例,但我无法让它工作。 What is the difference between overload and alias in Mockery?

我做错了什么?我更愿意让嘲弄别名工作,但实例模拟会很好。请指出我正确的方向。提前感谢您的帮助。

控制器

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

型号

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

测试

// 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

【问题讨论】:

    标签: laravel phpunit mockery


    【解决方案1】:

    您得到 500,这意味着代码有问题。只需扫描它,我就注意到您在Recordings 类上缺少filenameFromId 方法,并且测试正在创建一个名为$recordingMock 的模拟,但您尝试使用$urlMock。先尝试解决这些问题。
    然后你在模拟这个类,但你从来没有在你的应用程序中替换它(你显然在旧测试中做了它)。
    通常,您在模拟时要遵循以下步骤:
    1. 模拟类
    2. 告诉 Laravel 在有人请求时用你的 mock 替换该类
    3.对mock做出一些断言

    【讨论】:

    • 感谢您的回复。我不仅引用了错误的变量名($recordingMock / $urlMock),而且我模拟的类也不正确——“alias:Dreamonkey\cloudFrontSignedURL”应该是“alias:CloudFrontUrlSigner”。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-10-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多