Summer Sale! All accounts are 50% off this week.

Romain's avatar
Level 30

How to mock a dynamic class?

Hi,

I have this piece of code for which I want to mock the result of:

$scraper = $url->getScraper();
$media = new $scraper($this->article, $url);
$this->result = $media->scrape();

In my test I know which scraper will be used as it is derived from the url in the URL model, so I know which one to mock. But I can't find how to mock. As the class can't be passed in the service container, I can't find how to mock it.

Any help would be greatly appreciated.

0 likes
6 replies
Romain's avatar
Level 30

@tisuchi This is definitely a step forward, thanks for letting me know. Now the docs assumes that I then call the stub right after declaring it, but it's not my case.

Maybe I wasn't clear, but the call to the class I want mocked is within another class (a Job to be precise), but in my test I want to mock the scraper class so the code can continue executing and my test can find out the final value of the execution.

Basically:

Job.php:

// Returns MyScraper
$scraper = $url->getScraper();
$media = new $scraper($this->article, $url);
$this->result = $media->scrape();
// Here I need $this->result to equal 100 so the code below can keep executing

if($this->result !== 0){...

Test.php:

$mockScraper = $this->createMock(MyScraper::class);
$mockScraper->method('scrape')->willReturn(100);

With that code, the scraper is not mocked in Job.php.

tisuchi's avatar

@Romain How about this?

$mockScraper = $this->createMock(MyScraper::class);
$mockScraper->method('scrape')->willReturn(100);

app()->bind(MyScraper::class, function () use ($mockScraper) {
    return $mockScraper;
});

// Now when you run your job, it will use the mock scraper instead of the real one
$job = new Job();
$job->run();
1 like
Romain's avatar
Level 30

@tisuchi Im afraid this still doesn't work as expected. I will share a bit more of the code, maybe I missed something.

Test:

$mockScraper = $this->createMock(MyScraper::class);

        $stockResult = new StockResult(collect(
            ['price' => 1000]
        ));

        $mockScraper->method('scrape')->willReturn($stockResult);
        $article = Article::factory()->hasUrls(1, [
            'url' => 'http://myscraper.com'
        ])->create();

        app()->bind(MyScraper::class, function () use ($mockScraper) {
            return $mockScraper;
        });
        
        TrackSingleArticleStocks::dispatch($article);

TrackSingleArticleStocks

            // in the handle() method
           // Returns the scraper based on the URL passed in Artlcle
            $scraper = $url->getScraper();
            $media = new $scraper($this->article, $url);
            $this->result = $media->scrape();
            // Returns App\Scrapers\MyScrapper instance when running the test
            dd($media);
            // Return an object with price = null instead of 1000
           dd($this->results);

I hope this makes clearer what I am trying to do.

Majk's avatar

Hi, i have the same problem. Any news on this? @romain You solved it?

Best Regards!

Romain's avatar
Level 30

@Majk Hey,

I ended up shifting my approach a little. Instead of mocking the scrapper, I mock StockResult, which is what is returned by the scrapper. That allows me to simply set my return values, which is what I really wanted in the end.

Looks like that:

$this->mock(StockResult::class, function (MockInterface $mock) {
            $mock->shouldReceive('calculate')
                 ->times(4)
                 ->andReturn(collect([
                    'price' => 1000,
                    'currency' => 'USD'
                 ]),
                 collect([
                    'price' => 800,
                    'currency' => 'USD'
                ])
            );
        });

        TrackSingleArticleStocks::dispatch($article);

        $this->travel(1)->days();

        TrackSingleArticleStocks::dispatch($article);

Hope that helps you

1 like

Please or to participate in this conversation.