mardi 9 juin 2015

Mockery mocked Laravel model still attempting Db call during unit test

The issue

I am writing a simple test for a Laravel repository, which is used to get different products from a database. The repository currently works fine, but writing tests is proving very difficult.

Depending on which code I write, there are two different issues.

1) If I instantiate a new Mock of a Class, the method I am trying to call is not recognised.

2) If I reference an existing instance of the Mock that was created in the setUp method of the test, the Mock tries to connect to a database instead of using the mocked method.

The files

My project general setup is as follows:

  • Product.php (model)
  • DbProductRepository.php (repository)
  • DbProductRepositoryTest.php (test)

The code

DbProductRepository.php

// Return all products, where an optionally set 'purchasable' flag is set
public function loadAll($only_purchasable = null)
{
    return $this->product
                ->with('prices.currency')
                ->where(($only_purchasable ? 'purchasable' : 'cat_id'), ($only_purchasable ? '=' : '>='), ($only_purchasable ? true : '0'))
                ->orderBy('cat_order', 'asc')
                ->get();
}

The repository method works fine, but I would like to write a test as an exercise to see how that would be done.

DbProductRepositoryTest.php

use App\Repositories\Product\DbProductRepository;

class DbProductRepositoryTest extends TestCase{

protected $repository;
protected $category_hierarchy;
protected $currency;
protected $product;
protected $request;
protected $cache;
protected $collection;

public function setUp()
{
    parent::setUp();

    $this->collection           = $this->mock('Illuminate\Database\Eloquent\Collection');
    $this->category_hierarchy   = $this->mock('App\Helpers\CategoryHierarchy');
    $this->cache                = $this->mock('Illuminate\Cache\Repository');
    $this->request              = $this->mock('Illuminate\Http\Request');
    $this->currency             = $this->mock('App\Currency');
    $this->product              = $this->mock('App\Product');
}

public function tearDown()
{
    parent::tearDown();
    Mockery::close();
}

public function mock($class, $partial = false)
{
    if($partial)
    {
        $mock = Mockery::mock($class)->makePartial();
    }
    else
    {
        $mock = Mockery::mock($class);
    }
    $this->app->instance($class, $mock);
    return $mock;
}

/**
 * @test
 */
public function it_returns_all_products()
{
    // Given there are 5 products.
    $productA = $this->createMockProduct();
    $productB = $this->createMockProduct();
    $productC = $this->createMockProduct();
    $productD = $this->createMockProduct();
    $productE = $this->createMockProduct();

    // Set up the expected return Collection.
    $collection = $this->setupExpectedCollection([$productA, $productB, $productC, $productD, $productE]);

    // Mock the model that performs the db call.
    $model = $this->product;

    $model->shouldReceive('with')->once()->andReturnSelf();
    $model->shouldReceive('where')->once()->andReturnSelf();
    $model->shouldReceive('orderBy')->once()->andReturnSelf();
    $model->shouldReceive('get')->once()->andReturn($collection);

    // Instantiate the repository.
    $repository = new DbProductRepository(
        $this->category_hierarchy,
        $this->currency,
        $model,
        $this->request,
        $this->cache
    );

    // When I request all products.
    $products = $repository->loadAll();

    // Then a Collection with all the products is returned.
    $this->assertCount(5, $products);
}

private function createMockProduct($properties = null, $partial = true)
{
    $product = $this->mock('App\Product', $partial);
    if(is_array($properties))
    {
        foreach($properties as $key=>$value)
        {
            $product->$key = $value;
        }
    }
    return $product;
}

private function setupExpectedCollection(array $data)
{
    $collection = new Illuminate\Database\Eloquent\Collection();
    foreach($data as $item)
    {
        $collection->add($item);
    }
    return $collection;
}

The problem

The mock for the model is not being created properly I believe, as the error message I get when running the unit test is:

1) DbProductRepositoryTest::it_returns_all_products
InvalidArgumentException: Database does not exist.
/var/www/html/laravel/vendor/laravel/framework/src/Illuminate/Database/Connectors/SQLiteConnector.php:34
/var/www/html/laravel/vendor/laravel/framework/src/Illuminate/Database/Connectors/ConnectionFactory.php:58
/var/www/html/laravel/vendor/laravel/framework/src/Illuminate/Database/Connectors/ConnectionFactory.php:47
/var/www/html/laravel/vendor/laravel/framework/src/Illuminate/Database/DatabaseManager.php:177
/var/www/html/laravel/vendor/laravel/framework/src/Illuminate/Database/DatabaseManager.php:65
/var/www/html/laravel/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:3146
/var/www/html/laravel/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:3112
/var/www/html/laravel/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:1887
/var/www/html/laravel/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:1828
/var/www/html/laravel/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:1802
/var/www/html/laravel/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:882
/var/www/html/laravel/app/Product.php:97
/var/www/html/laravel/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:2638
/var/www/html/laravel/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:2573
/var/www/html/laravel/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:3263
/var/www/html/laravel/app/Repositories/Product/DbProductRepository.php:257
/var/www/html/laravel/app/Repositories/Product/DbProductRepository.php:70
/var/www/html/laravel/tests/Repositories/Product/DbProductRepositoryTest.php:81

Line 97 - Product.php

public function prices()
{
    return $this->hasMany('App\Price', 'product_id', 'cat_id');
}

However...

I have written another test for a different method in the ProductRepository, where the model is being properly mocked and a connection to the database isn't being attempted:

/**
 * @test
 */
public function it_returns_all_categories_that_dispatch_emails()
{
    // Given there are three products and two of them dispatch emails.
    $productA = $this->createMockProduct(['dispatches_emails'=>true]);
    $productB = $this->createMockProduct(['dispatches_emails'=>true]);
    $productC = $this->createMockProduct(['dispatches_emails'=>false]);

    // Set up the expected return Collection.
    $collection = $this->setupExpectedCollection([$productA, $productB]);

    // Mock the model that performs the db call.
    $model = Mockery::mock('App\Product');

    // Set up the model expectations.
    $model->shouldReceive('where')->once()->with('dispatches_emails', true)->andReturnSelf();
    $model->shouldReceive('get')->once()->andReturn($collection);

    // Instantiate the repository.
    $repository = new DbProductRepository(
        $this->category_hierarchy,
        $this->currency,
        $model,
        $this->request,
        $this->cache
    );

    // When I request the categories which dispatches emails from the repository.
    $products = $repository->getDispatchesEmailCategories();

    // Then a Collection with two products dispatcher products is returned.
    $this->assertEquals($collection, $products);

    // And the product which doesn't dispatch e-mails is excluded.
    $this->assertNotContains($productC, $products);

}

The related (working) method

public function getDispatchesEmailCategories()
{
    return $this->product->where('dispatches_emails', true)->get();
}

Clearly a much simpler query being run by the repository, but I don't see why this test works but the previous doesn't.

One quirk I've come across, is that on the first test - if I change the $model from:

$model = $this->product;

to:

$model = Mockery::mock('App\Product');

Then, the mock appears to not use the Database - but complains with the following error:

BadMethodCallException: Static method Mockery_5_App_Product::with() does not exist on this mock object

Aucun commentaire:

Enregistrer un commentaire