jaycito's avatar

Test uses real interface instead of mock object

I'm on L10.x testing a class Product that calls a service ProductAPI. I'm trying to mock the API call inside ProductAPI (it isn't the focus of the test) and provide Product with fake data from the "API" to test its behaviour.

So far no matter what I've done in my test, it always calls the real method on the service class instead of my mock setup. Here's some code samples, simplified for brevity.

Product class using the API service class

public function refresh(){
  $apiQuery = '...';
  $records = (new ProductAPI())->queryRequest($apiQuery); // $records is what I want to fake to test the refresh method
}

ProductTest class

public function testRefresh(){
  $fakeData = [...]; // array of fake api results
  $this->mockObject = $this->mock(ProductAPI::class, function (MockInterface $mock) use ($fakeData) {
      $mock->shouldReceive('queryRequest')->once()->andReturn((object)$fakeData);
  });

  (new Product())->refresh(); // always ends up calling the real queryRequest method
  $this->assertDatabaseCount('products',  2); // inevitably fails
}
0 likes
1 reply
LaryAI's avatar
LaryAI
AI
Best Answer
Level 58

The issue you're encountering is likely due to the fact that you're instantiating the ProductAPI class directly within the Product class, which makes it difficult to mock. To properly mock the ProductAPI service, you should use dependency injection to inject the ProductAPI instance into the Product class. This way, you can easily replace the real ProductAPI instance with a mock in your tests.

Here's how you can refactor your code to use dependency injection:

Step 1: Refactor the Product class to use dependency injection

class Product
{
    protected $productAPI;

    public function __construct(ProductAPI $productAPI)
    {
        $this->productAPI = $productAPI;
    }

    public function refresh()
    {
        $apiQuery = '...';
        $records = $this->productAPI->queryRequest($apiQuery); // $records is what I want to fake to test the refresh method
        // Process $records...
    }
}

Step 2: Update the ProductTest class to inject the mock

use Mockery\MockInterface;

class ProductTest extends TestCase
{
    public function testRefresh()
    {
        $fakeData = [...]; // array of fake api results

        $mockProductAPI = $this->mock(ProductAPI::class, function (MockInterface $mock) use ($fakeData) {
            $mock->shouldReceive('queryRequest')->once()->andReturn((object)$fakeData);
        });

        $product = new Product($mockProductAPI);
        $product->refresh(); // This should now use the mocked queryRequest method

        $this->assertDatabaseCount('products', 2); // Adjust this assertion as needed
    }
}

Explanation

  1. Dependency Injection in Product Class: The Product class now accepts a ProductAPI instance through its constructor. This allows you to inject a mock instance during testing.

  2. Mocking in ProductTest Class: In the test, you create a mock of the ProductAPI class and configure it to return the fake data when the queryRequest method is called. You then inject this mock into the Product instance.

By following these steps, you ensure that the Product class uses the mocked ProductAPI instance during testing, allowing you to control the behavior of the queryRequest method and test the refresh method effectively.

1 like

Please or to participate in this conversation.