Unit Test Fails __construct() must be an instance of Aws\S3\S3ClientInterface, instance of Double\stdClass\P1 given

Working on my first Laravel packag . It is a Flysystem Adapter. It is the Amazon S3 adapter, but one I want to improve to deal with 503 rate limiting errors when storing Laravel Spatie Backups on Digital Ocean Spaces.

I also added my first PHP Unit Test:

<?php

namespace Smart48\FlysystemDigitalOcean\Test;

use GuzzleHttp\Psr7\Response;
use League\Flysystem\Config;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Smart48\FlysystemDigitalOcean\DigitalOceanAdapter;

class DigitalOceanAdapterTest extends TestCase
{
    /** @var \Aws\S3\S3Client\Client|\Prophecy\Prophecy\ObjectProphecy */
    protected $client;

    /** @var \Smart48\FlysystemDigitalOcean\DigitalOceanAdapter */
    protected $digitalOceanAdapter;

    /**
     * @before
     */
    public function setUpTest()
    {
        $this->client = $this->prophesize(Client::class);

        $this->digitalOceanAdapter = new DigitalOceanAdapter($this->client->reveal(), 'prefix');
    }

    /** @test */
    public function it_can_write()
    {
        $this->client->upload(Argument::any(), Argument::any(), Argument::any())->willReturn([
            'server_modified' => '2015-05-12T15:50:38Z',
            'path_display' => '/prefix/something',
            '.tag' => 'file',
        ]);

        $result = $this->digitalOceanAdapter->write('something', 'contents', new Config());

        $this->assertIsArrayType($result);
        $this->assertArrayHasKey('type', $result);
        $this->assertEquals('file', $result['type']);
    }

    /** @test */
    public function it_can_update()
    {
        $this->client->upload(Argument::any(), Argument::any(), Argument::any())->willReturn([
            'server_modified' => '2015-05-12T15:50:38Z',
            'path_display' => '/prefix/something',
            '.tag' => 'file',
        ]);

        $result = $this->digitalOceanAdapter->update('something', 'contents', new Config());

        $this->assertIsArrayType($result);
        $this->assertArrayHasKey('type', $result);
        $this->assertEquals('file', $result['type']);
    }

    /** @test */
    public function it_can_write_a_stream()
    {
        $this->client->upload(Argument::any(), Argument::any(), Argument::any())->willReturn([
            'server_modified' => '2015-05-12T15:50:38Z',
            'path_display' => '/prefix/something',
            '.tag' => 'file',
        ]);

        $result = $this->digitalOceanAdapter->writeStream('something', tmpfile(), new Config());

        $this->assertIsArrayType($result);
        $this->assertArrayHasKey('type', $result);
        $this->assertEquals('file', $result['type']);
    }

    /** @test */
    public function it_can_upload_using_a_stream()
    {
        $this->client->upload(Argument::any(), Argument::any(), Argument::any())->willReturn([
            'server_modified' => '2015-05-12T15:50:38Z',
            'path_display' => '/prefix/something',
            '.tag' => 'file',
        ]);

        $result = $this->digitalOceanAdapter->updateStream('something', tmpfile(), new Config());

        $this->assertIsArrayType($result);
        $this->assertArrayHasKey('type', $result);
        $this->assertEquals('file', $result['type']);
    }

    /**
     * @test
     *
     * @dataProvider  metadataProvider
     */
    public function it_has_calls_to_get_meta_data($method)
    {
        $this->client = $this->prophesize(Client::class);
        $this->client->getMetadata('/one')->willReturn([
            '.tag'   => 'file',
            'server_modified' => '2015-05-12T15:50:38Z',
            'path_display' => '/one',
        ]);

        $this->digitalOceanAdapter = new DigitalOceanAdapter($this->client->reveal());

        $this->assertIsArrayType($this->digitalOceanAdapter->{$method}('one'));
    }

    public function metadataProvider(): array
    {
        return [
            ['getMetadata'],
            ['getTimestamp'],
            ['getSize'],
            ['has'],
        ];
    }

    /** @test */
    public function it_will_not_hold_metadata_after_failing()
    {
        $this->client = $this->prophesize(Client::class);

        $this->client->getMetadata('/one')->willThrow(new BadRequest(new Response(409)));

        $this->digitalOceanAdapter = new DigitalOceanAdapter($this->client->reveal());

        $this->assertFalse($this->digitalOceanAdapter->has('one'));
    }

    /** @test */
    public function it_can_read()
    {
        $stream = tmpfile();
        fwrite($stream, 'something');

        $this->client->download(Argument::any(), Argument::any())->willReturn($stream);

        $this->assertIsArrayType($this->digitalOceanAdapter->read('something'));
    }

    /** @test */
    public function it_can_read_using_a_stream()
    {
        $stream = tmpfile();
        fwrite($stream, 'something');

        $this->client->download(Argument::any(), Argument::any())->willReturn($stream);

        $this->assertIsArrayType($this->digitalOceanAdapter->readStream('something'));

        fclose($stream);
    }

    /** @test */
    public function it_can_delete_stuff()
    {
        $this->client->delete('/prefix/something')->willReturn(['.tag' => 'file']);

        $this->assertTrue($this->digitalOceanAdapter->delete('something'));
        $this->assertTrue($this->digitalOceanAdapter->deleteDir('something'));
    }

    /** @test */
    public function it_can_create_a_directory()
    {
        $this->client->createFolder('/prefix/fail/please')->willThrow(new BadRequest(new Response(409)));
        $this->client->createFolder('/prefix/pass/please')->willReturn([
            '.tag' => 'folder',
            'path_display'   => '/prefix/pass/please',
        ]);

        $this->assertFalse($this->digitalOceanAdapter->createDir('fail/please', new Config()));

        $expected = ['path' => 'pass/please', 'type' => 'dir'];
        $this->assertEquals($expected, $this->digitalOceanAdapter->createDir('pass/please', new Config()));
    }

    /** @test */
    public function it_can_list_a_single_page_of_contents()
    {
        $this->client->listFolder(Argument::type('string'), Argument::any())->willReturn(
            [
                'entries' => [
                    ['.tag' => 'folder', 'path_display' => 'dirname'],
                    ['.tag' => 'file', 'path_display' => 'dirname/file'],
                ],
                'has_more' => false,
            ]
        );

        $result = $this->digitalOceanAdapter->listContents('', true);

        $this->assertCount(2, $result);
    }

    /** @test */
    public function it_can_list_multiple_pages_of_contents()
    {
        $cursor = 'cursor';

        $this->client->listFolder(Argument::type('string'), Argument::any())->willReturn(
            [
                'entries' => [
                    ['.tag' => 'folder', 'path_display' => 'dirname'],
                    ['.tag' => 'file', 'path_display' => 'dirname/file'],
                ],
                'has_more' => true,
                'cursor' => $cursor,
            ]
        );

        $this->client->listFolderContinue(Argument::exact($cursor))->willReturn(
            [
                'entries' => [
                    ['.tag' => 'folder', 'path_display' => 'dirname2'],
                    ['.tag' => 'file', 'path_display' => 'dirname2/file2'],
                ],
                'has_more' => false,
            ]
        );

        $result = $this->digitalOceanAdapter->listContents('', true);

        $this->assertCount(4, $result);
    }

    /** @test */
    public function it_can_rename_stuff()
    {
        $this->client->move(Argument::type('string'), Argument::type('string'))->willReturn(['.tag' => 'file', 'path' => 'something']);

        $this->assertTrue($this->digitalOceanAdapter->rename('something', 'something'));
    }

    /** @test */
    public function it_will_return_false_when_a_rename_has_failed()
    {
        $this->client->move('/prefix/something', '/prefix/something')->willThrow(new BadRequest(new Response(409)));

        $this->assertFalse($this->digitalOceanAdapter->rename('something', 'something'));
    }

    /** @test */
    public function it_can_copy_a_file()
    {
        $this->client->copy(Argument::type('string'), Argument::type('string'))->willReturn(['.tag' => 'file', 'path' => 'something']);

        $this->assertTrue($this->digitalOceanAdapter->copy('something', 'something'));
    }

    /** @test */
    public function it_will_return_false_when_the_copy_process_has_failed()
    {
        $this->client->copy(Argument::any(), Argument::any())->willThrow(new BadRequest(new Response(409)));

        $this->assertFalse($this->digitalOceanAdapter->copy('something', 'something'));
    }

    /** @test */
    public function it_can_get_a_client()
    {
        $this->assertInstanceOf(Client::class, $this->digitalOceanAdapter->getClient());
    }

    private function assertIsArrayType($input)
    {
        if (method_exists(TestCase::class, 'assertIsArray')) {
            $this->assertIsArray($input);
        } else {
            $this->assertInternalType('array', $input);
        }
    }
}

But running it I get this error

➜  flysystem-digital-ocean-spaces git:(main) ✗ composer test tests/DigitalOceanAdapterTest.php
> vendor/bin/phpunit 'tests/DigitalOceanAdapterTest.php'
PHPUnit 9.5.4 by Sebastian Bergmann and contributors.

EEEEEEEEEEEEEEEEEEEE                                              20 / 20 (100%)

Time: 00:00.048, Memory: 6.00 MB

There were 20 errors:

1) Smart48\FlysystemDigitalOcean\Test\DigitalOceanAdapterTest::it_can_write
TypeError: Argument 1 passed to Smart48\FlysystemDigitalOcean\DigitalOceanAdapter::__construct() must be an instance of Aws\S3\S3ClientInterface, instance of Double\stdClass\P1 given, called in /Users/jasperfrumau/code/flysystem-digital-ocean-spaces/tests/DigitalOceanAdapterTest.php on line 28
....

I thought the actual new adapter was setup just fine so… S3ClientInterface should be loaded and used. But the test says otherwise

_construct() must be an instance of Aws\S3\S3ClientInterface, instance of Double\stdClass\P1 given

But how can I debug this? First time working on a package, but also first time unit testing. So any pointers would be great.

You’re tightly binding your code. This is where dependency inversion and coding to an interface comes in with SOLID principles.

1 Like

I think it maybe coming from Prophet. Prophet returns something like Double\stdClass\P1 when you don’t have the mocked class loaded in the PHP process.

1 Like
Sponsor our Newsletter | Privacy Policy | Terms of Service