Mocked protected method not returning value set in mock

184 Views Asked by At

I have a class called ModuleManifest with this constructor:

/**
 * Create a new instance of the ModuleManifest class.
 *
 * @param string $vendor The vendor of the module.
 * @param string $package The name of the package.
 * @return void
 */
public function __construct(string $vendor, string $package)
{
    $manifest = $this->manifestPath($vendor, $package);

    if (!File::exists($manifest))
    {
        throw new \InvalidArgumentException(sprintf(
            'Package %s from vendor %s does not contain a manifest',
            $package, $vendor
        ));
    }

    $this->manifest = $this->load($manifest);
}

This constructor loads a json file with the load method.

/**
 * Load a module manifest into an object.
 *
 * @param string $path The path to the module.
 * @return object
 */
protected function load(string $path) : object
{
    return json_decode(file_get_contents($path));
}

/**
 * Determine the path to the module manifest.
 *
 * @param string $vendor The vendor of the module.
 * @param string $package The name of the package.
 * @return string
 */
protected function manifestPath(string $vendor, string $package) : string
{
    return implode('/', [base_path('vendor'), $vendor, $package, 'resources/manifest.json']);
}

I want determine whether the method is returning an object:

/** @testdox Returns an object when the path exists */
public function test_manifest_returns_object() : void
{
    File::shouldReceive('exists')->once()->andReturnTrue();

    $this->mock(ModuleManifest::class, function (MockInterface $mock) {
        $mock->shouldAllowMockingProtectedMethods();
        $mock->shouldReceive('load')->once()->andReturn(
            json_decode('{"name": "testModule"}')
        );
    });

    $manifest = new ModuleManifest('vendor', 'package');

    $this->assertTrue($manifest->has('name'));
}

I get the error:

ErrorException: file_get_contents(...): Failed to open stream: No such file or directory

I would expect it to return the result of json_decode('{"name": "testModule"}'), but it tries to call the function normally. How to resolve this? I have tried to create a partial mock, but the issue remains.

1

There are 1 best solutions below

0
Olivier On

Here is how I would do it. Add an optional $basePath parameter to the constructor whose default value is base_path():

class ModuleManifest
{
    private object $manifest;

    public function __construct(string $vendor, string $package, string $basePath='')
    {
        if ($basePath=='')
            $basePath = base_path();
        $path = $this->manifestPath($vendor, $package, $basePath);

        if (!File::exists($path))
        {
            throw new \InvalidArgumentException(sprintf(
                'Package %s from vendor %s does not contain a manifest',
                $package, $vendor
            ));
        }

        $this->manifest = $this->load($path);
    }

    protected function manifestPath(string $vendor, string $package, string $basePath) : string
    {
        return implode('/', [$basePath, 'vendor', $vendor, $package, 'resources/manifest.json']);
    }

    protected function load(string $path) : object
    {
        return json_decode(file_get_contents($path));
    }

    public function has(string $key) : bool
    {
        return isset($this->manifest->$key);
    }
}

For the test, choose a test directory and create vendor/foo/bar/resources/manifest.json in it. Then the testing code is simply:

public function test_manifest_returns_object() : void
{
    $manifest = new ModuleManifest('foo', 'bar', '/path/to/test/dir');
    $this->assertTrue($manifest->has('name'));
}

That's it. You don't need any mock. Also note that you should not test that ModuleManifest uses the File class because it's just an implementation detail. It could very well do it another way (such as calling file_exists() directly).