There’s a little trick I like to use, when writing tests. Before I share it, I need to explain why I would need to use it. Imagine you were asked to test the following class:

<?php



namespace Acme;



class FilesystemCacheAdapter

implements CacheAdapterInterface

{

protected $config;



public function __construct(ConfigInterface $config)

{

$this->config = $config;

}



public function readFile($name)

{

$path = $this->config->get("cache.path");

$file = $path . "/" . $name;



if (file_exists($file) && $this->fileIsValid($file)) {

return file_get_contents($file);

}



return null;

}



protected function fileIsValid($file)

{

$expires = $this->config->get("cache.expires");



return filemtime($file) + $expires > time();

}

}

You may begin by mocking the ConfigInterface dependency (using something like Mockery):





namespace Acme;



use Mockery;



class FilesystemCacheAdapterTest

extends PHPUnit_Framework_TestCase

{

public function tearDown()

{

Mockery::close();

}



/**

public function returns_null_for_non_existent_file()

{

$config = Mockery::mock("ConfigInterface");

$config->shouldReceive("get")->andReturn("404");



$adapter = new FilesystemCacheAdapter($config);



$this->assertNull($adapter->readFile("foo"));

}

} @test */public function returns_null_for_non_existent_file()$config = Mockery::mock("ConfigInterface");$config->shouldReceive("get")->andReturn("404");$adapter = new FilesystemCacheAdapter($config);$this->assertNull($adapter->readFile("foo"));

That may work for the path of testing that says; “if the file isn’t found then the method should return null”. What about if we want to test whether the cache file is expired?

The functions we are using are those native filesystem functions that ship with PHP. We can’t even throw ApectMock (https://github.com/

Codeception/AspectMock) at them because we don’t have access to that source code.

We could depend on a filesystem library — there are some excellent, well tested ones like Flysystem (http://flysystem.thephpleague.com). Then we wouldn’t need to worry about the testing because someone else has done that work.

This introduces an extra dependency, and only really solves the problem of using file_exists(), filemtime() and file_get_contents(). What about time()..?

If you’re using the same namespace for your library code and your test code, you could try this trick:





namespace Acme;



function time()

{

return 30;

}



function filemtime($file)

{

if ($file == "foo/bar") {

return 15;

}



return 25;

}



function file_get_contents()

{

return "mocked file_get_contents";

}



function file_exists()

{

if ($file == "foo/bar") {

return false;

}



return true;

}



class FilesystemCacheAdapterTest

extends PHPUnit_Framework_TestCase

{

public function tearDown()

{

Mockery::close();

}



/**

public function returns_null_for_non_existant_file()

{

$config = Mockery::mock("ConfigInterface");

$config->shouldReceive("get")->andReturn("foo");



$adapter = new FilesystemCacheAdapter($config);



$this->assertNull($adapter->readFile("bar"));

}



/**

public function returns_null_for_expired_file()

{

$config = Mockery::mock("ConfigInterface");



$config

->shouldReceive("get")

->with("config.path")

->andReturn("foo");



$config

->shouldReceive("get")

->with("config.expire")

->andReturn(10);



$adapter = new FilesystemCacheAdapter($config);



$this->assertNull($adapter->readFile("bar"));

}



/**

public function returns_mocked_for_existing_file()

{

$config = Mockery::mock("ConfigInterface");



$config

->shouldReceive("get")

->with("config.path")

->andReturn("bar");



$config

->shouldReceive("get")

->with("config.expire")

->andReturn(10);



$adapter = new FilesystemCacheAdapter($config);



$this->assertEquals(

"mocked file_get_contents",

$adapter->readFile("baz")

);

}

} @test */public function returns_null_for_non_existant_file()$config = Mockery::mock("ConfigInterface");$config->shouldReceive("get")->andReturn("foo");$adapter = new FilesystemCacheAdapter($config);$this->assertNull($adapter->readFile("bar"));/** @test */public function returns_null_for_expired_file()$config = Mockery::mock("ConfigInterface");$config->shouldReceive("get")->with("config.path")->andReturn("foo");$config->shouldReceive("get")->with("config.expire")->andReturn(10);$adapter = new FilesystemCacheAdapter($config);$this->assertNull($adapter->readFile("bar"));/** @test */public function returns_mocked_for_existing_file()$config = Mockery::mock("ConfigInterface");$config->shouldReceive("get")->with("config.path")->andReturn("bar");$config->shouldReceive("get")->with("config.expire")->andReturn(10);$adapter = new FilesystemCacheAdapter($config);$this->assertEquals("mocked file_get_contents",$adapter->readFile("baz"));

Say what? If both classes are in the same namespace, then you can define local-scope functions (with the same names as the built-in functions the code calls) and they will be run instead of the native functions.

This will not work when the code you are trying to test uses fully-qualified function names like \time() or \file_get_contents().

I have found it super useful for testing whether certain headers were set (without needing to install xdebug) or for swapping out the time() function. It’s not something you should be depending on in a production system though…