Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHPUnit - Mock S3Client not working well

Library: "aws/aws-sdk-php": "2.*"
PHP version: PHP 5.4.24 (cli)

composer.json

{
    "require": {
        "php": ">=5.3.1",
        "aws/aws-sdk-php": "2.*",
        ...
    },

    "require-dev": {
        "phpunit/phpunit": "4.1",
        "davedevelopment/phpmig": "*",
        "anahkiasen/rocketeer": "*"
    },
    ...
}

We have made a AwsWrapper to get the functional actions: uploadFile, deleteFile...
You can read the class, with dependency injection to be unit tested.
Focus on constructor and inner $this->s3Client->putObject(...) call on uploadFile function.

<?php

namespace app\lib\modules\files;

use Aws\Common\Aws;
use Aws\S3\Exception\S3Exception;
use Aws\S3\S3Client;
use core\lib\exceptions\WSException;
use core\lib\Injector;
use core\lib\utils\System;

class AwsWrapper
{

  /**
   * @var \core\lib\Injector
   */
  private $injector;

  /**
   * @var S3Client
   */
  private $s3Client;

  /**
   * @var string
   */
  private $bucket;

  function __construct(Injector $injector = null, S3Client $s3 = null)
  {
    if( $s3 == null )
    {
      $aws = Aws::factory(dirname(__FILE__) . '/../../../../config/aws-config.php');
      $s3 = $aws->get('s3');
    }
    if($injector == null)
    {
      $injector = new Injector();
    }
    $this->s3Client = $s3;
    $this->bucket = \core\providers\Aws::getInstance()->getBucket();
    $this->injector = $injector;
  }

  /**
   * @param $key
   * @param $filePath
   *
   * @return \Guzzle\Service\Resource\Model
   * @throws \core\lib\exceptions\WSException
   */
  public function uploadFile($key, $filePath)
  {
    /** @var System $system */
    $system = $this->injector->get('core\lib\utils\System');
    $body   = $system->fOpen($filePath, 'r');
    try {
      $result = $this->s3Client->putObject(array(
        'Bucket' => $this->bucket,
        'Key'    => $key,
        'Body'   => $body,
        'ACL'    => 'public-read',
      ));
    }
    catch (S3Exception $e)
    {
      throw new WSException($e->getMessage(), 201, $e);
    }

    return $result;
  }

} 

The testing file has our Injector and the S3Client instances as a PhpUnit MockObject. To mock S3Client we must disable original constructor with the Mock Builder.

To mock S3Client:

$this->s3Client = $this->getMockBuilder('Aws\S3\S3Client')->disableOriginalConstructor()->getMock();

To configure inner putObject call (case to test with putObject throw S3Exception, but we have the same problem with $this->returnValue($expected).

To init Test Class and configure sut:

  public function setUp()
  {
    $this->s3Client = $this->getMockBuilder('Aws\S3\S3Client')->disableOriginalConstructor()->getMock();
    $this->injector = $this->getMock('core\lib\Injector');
  }

  public function configureSut()
  {
    return new AwsWrapper($this->injector, $this->s3Client);
  }

Not working code:

$expectedArray = array(
  'Bucket' => Aws::getInstance()->getBucket(),
  'Key'    => $key,
  'Body'   => $body,
  'ACL'    => 'public-read',
);
$this->s3Client->expects($timesPutObject)
  ->method('putObject')
  ->with($expectedArray)
  ->will($this->throwException(new S3Exception($exceptionMessage, $exceptionCode)));
$this->configureSut()->uploadFile($key, $filePath);

When we execute our test function, the injected S3Client doesn't throw the exception or return the expected value, always return NULL.

With xdebug we have seen taht the S3Client MockObject is configured correctly but doesn't works as is configured at will().

A "solution" (or a bad solution)could be doing a S3ClientWrapper, this will only move the problem to other Class that couldn't be unit tested with mocks.

Any idea?

UPDATE Screenshot on configure MockObject with xdebug: enter image description here

like image 633
PituSabadí Avatar asked Jun 06 '14 11:06

PituSabadí


1 Answers

The following code works and passes as expected, so I don't think you are running into any limitations caused by PHPUnit or the AWS SDK.

<?php

namespace Aws\Tests;

use Aws\S3\Exception\S3Exception;
use Aws\S3\S3Client;
use Guzzle\Service\Resource\Model;

class MyTest extends \PHPUnit_Framework_TestCase
{
    public function testMockCanReturnResult()
    {
        $model = new Model([
            'Contents' => [
                ['Key' => 'Obj1'],
                ['Key' => 'Obj2'],
                ['Key' => 'Obj3'],
            ],
        ]);

        $client = $this->getMockBuilder('Aws\S3\S3Client')
            ->disableOriginalConstructor()
            ->setMethods(['listObjects'])
            ->getMock();
        $client->expects($this->once())
            ->method('listObjects')
            ->with(['Bucket' => 'foobar'])
            ->will($this->returnValue($model));

        /** @var S3Client $client */
        $result = $client->listObjects(['Bucket' => 'foobar']);

        $this->assertEquals(
            ['Obj1', 'Obj2', 'Obj3'],
            $result->getPath('Contents/*/Key')
        );
    }

    public function testMockCanThrowException()
    {
        $client = $this->getMockBuilder('Aws\S3\S3Client')
            ->disableOriginalConstructor()
            ->setMethods(['getObject'])
            ->getMock();
        $client->expects($this->once())
            ->method('getObject')
            ->with(['Bucket' => 'foobar'])
            ->will($this->throwException(new S3Exception('VALIDATION ERROR')));

        /** @var S3Client $client */
        $this->setExpectedException('Aws\S3\Exception\S3Exception');
        $client->getObject(['Bucket' => 'foobar']);
    }
}

You can also use the Guzzle MockPlugin if you just want to mock the response and don't care about mocking/stubbing the objects.

like image 73
Jeremy Lindblom Avatar answered Oct 18 '22 05:10

Jeremy Lindblom