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

garrettmassey's avatar

Package Development / Testing - ensure an object property is an instance of a specific class

Hello!

I'm working on my first ever package, and I'm really trying to keep a testing driven mindset while developing... and I'm struggling to understand how to emulate certain behaviors of the Laravel app in the package's tests.

The general idea is that the package has a main class Analytics and the Analytics object has a property called client, which should be an instance of another class, say Data\Api\Client.

The client credentials are configured in the .env file, so what I am trying to test is that the Analytics object is created properly and that the $client property is in fact an instance of Data\Api\Client.

What I have tried is this, but it gives me an error (shown below)

public function analytics_data_client()
{
	//mock creating an instance of Analytics
	//and then getting the client via getClient()
	//and assert that the client is an instance of BetaAnalyticsDataClient

	$this->mock(Analytics::class, function (MockInterface $mock) {
		$mock->shouldReceive('getClient')
			->andReturn(new Data\Api\Client());
	});
}

but I get the error:

1) GarrettMassey\Analytics\Tests\AnalyticsTest::analytics_data_client
ErrorException: file_get_contents(): Read of 8192 bytes failed with errno=21 Is a directory

with several other messages beneath.

Any ideas or tips to writing tests like this?

0 likes
18 replies
Sinnbeck's avatar

You are first off missing a leading slash here

new \Data\Api\Client

And I assume you are import the other two classes?

Can you show the class you are mocking?

garrettmassey's avatar

@Sinnbeck

Sure!

The class that I am mocking is actually the facade:

<?php

namespace GarrettMassey\Analytics\Facades;

use Illuminate\Support\Facades\Facade;

/**
  * @see \GarrettMassey\Analytics\Analytics
  */
class Analytics extends Facade
{
	protected static function getFacadeAccessor()
	{
    	return \GarrettMassey\Analytics\Analytics::class;
	}

}

and the actual analytics class is

	<?php

	namespace GarrettMassey\Analytics;

	use Carbon\CarbonImmutable;
	use GarrettMassey\Analytics\Parameters\Dimensions;
	use GarrettMassey\Analytics\Parameters\Metrics;
	use Data\Api\Client;
	use Illuminate\Support\Collection;

	class Analytics {

			public Client $client;

			public function __construct()
			{
				$this->client = resolve(Client::class);

				return $this;
			}

			public function getClient()
			{
				return $this->client;
			}
	}

and yes, the other classes are imported.

garrettmassey's avatar

@Sinnbeck I did read through that and I guess I don't understand exactly how to mock the facade then...

I tried rewriting the test to be this:

<?php
namespace GarrettMassey\Analytics\Tests;

use GarrettMassey\Analytics\Facades\Analytics;
use Data\Api\Client;
use Illuminate\Support\Facades\App;
use Mockery;
use Mockery\MockInterface;

class AnalyticsTest extends TestCase
{

	/** @test */
	public function getClient_returns_GAClient()
	{
		Analytics::shouldReceive('getClient')
			->once()
			->andReturn(Mockery::mock(Client::class));
	}
}

and now when running composer test I get this error:

1) GarrettMassey\Analytics\Tests\AnalyticsTest::getClient_returns_GAClient ErrorException: file_get_contents(): Read of 8192 bytes failed with errno=21 Is a directory

Sinnbeck's avatar

@garrettmassey Are you able to run any tests at all? That error message is really weird and does not seem to be anyway related to the test

$this->assertTrue(true);
garrettmassey's avatar

@Sinnbeck Yes, another collaborator wrote tests for a different class in the package, and those all run just fine, and replacing the test I wrote above with $this->assertTrue(true); works just fine as well.

garrettmassey's avatar

@Sinnbeck it is, but it's private right now because it's not quite stable for public release. I can send an invite though

garrettmassey's avatar

@Sinnbeck The branch I am working on is MakeCredentialPathConfigurable, the main branch is behind dev right now, and I was going to set up pull request rules for main so that we didn't have to worry about managing a dev and main branch all the time.

Sinnbeck's avatar

@garrettmassey Ah ok checking that now.. First of all I notice you seem a bit off with how facades work. They are meant to new up a class and call the methods on that class. But query() is static? So there is in this case no need to use the facade.. Or dont call query() (just call Analytics::getClient())

garrettmassey's avatar

@Sinnbeck query() is static, with the intention that someone can call Analytics::query() and then chain methods on that like: Analytics::query()->methodNameHere()->run()

But yes, Facades are confusing to me... I am still learning! :)

Sinnbeck's avatar

@garrettmassey Yeah. Just saying it isnt needed when using facades ;) Its meant to chain into a dynamic method, not static..

Anyways. I found the line that is throwing the error. Never used the google analytics package, so I havent checked where it is set.

This line: https://github.com/googleapis/google-auth-library-php/blob/main/src/CredentialsLoader.php#L81

Gets the path /path/to/Analytics/vendor/orchestra/testbench-core/laravel, which isnt a credentials file

Edit: Ah I suppose they are set on this line. But those are never set correctly in the test

putenv('GOOGLE_APPLICATION_CREDENTIALS=' . base_path() . config('analytics.credentials_path') . config('analytics.credentials_file'));

https://packages.tools/testbench/basic/define-environment.html

garrettmassey's avatar

@Sinnbeck Ohhh. I think I understand, because in the Analytics class, the constructor takes .env variables and builds a path to a json file with the credentials the user downloaded from the Google Cloud Console.

So if I wanted to test this, the current Analytics constructor is:

public function __construct()
{
	putenv('GOOGLE_APPLICATION_CREDENTIALS=' . base_path() . config('analytics.credentials_path') . config('analytics.credentials_file'));
	$this->client = resolve(BetaAnalyticsDataClient::class);
	$this->propertyID = config('analytics.property_id');
	$this->dimensions = collect([]);
	$this->metrics = collect([]);
	$this->dateRanges = collect([]);

	return $this;
}

the base_path() is what's messing it up, right? how do I get the laravel application base path instead of the path in the package?

Sinnbeck's avatar
Sinnbeck
Best Answer
Level 102

@garrettmassey I would just set the full path in the config. Then you can overwrite it in the test

	putenv('GOOGLE_APPLICATION_CREDENTIALS=' .  config('analytics.credentials_path') . config('analytics.credentials_file'));

and then in config

    'credentials_path' => base_path(env('ANALYTICS_CREDENTIALS_PATH')),

Check the link above to testbench on how to set the config in tests

1 like
Sinnbeck's avatar

I also would suggest you add a check to your own code that ensuresf the file exists, and gives a usable error. Just so your users dont get the same issue

Please or to participate in this conversation.