Skip to content

Instantly share code, notes, and snippets.

@mdwheele
Last active November 17, 2015 20:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mdwheele/1d5034ef22885eceb4f9 to your computer and use it in GitHub Desktop.
Save mdwheele/1d5034ef22885eceb4f9 to your computer and use it in GitHub Desktop.
Useful trick to stub stable WordPress functions for testing your plugin boundaries in isolation.
<?php
namespace Vendor\Services;
use Closure;
class Cache
{
protected $prefix;
public function __construct($prefix = 'vendor_cache__')
{
$this->prefix = $prefix;
}
/**
* Checks the cache for a specific item.
*
* @param $key
* @return bool
*/
public function has($key)
{
return $this->get($key) !== null;
}
/**
* Retrieves a value from the cache.
*
* @param string $key Key representing where value is stored.
* @return mixed|null returns value if key exists and is current, null otherwise.
*/
public function get($key)
{
$value = get_transient($this->prefix.$key);
if ($value === false) {
return null;
}
return $value;
}
/**
* Stores an item in the cache for some amount of time.
*
* @param string $key
* @param mixed $item
* @param int $minutes
* @return bool
*/
public function put($key, $item, $minutes = 1)
{
return set_transient($this->prefix.$key, $item, $minutes * 60);
}
/**
* Remembers the return value of a closure for some amount of time.
*
* @param string $key
* @param int $minutes
* @param Closure $callback
* @return mixed
*/
public function remember($key, $minutes, Closure $callback)
{
if (!is_null($value = $this->get($key))) {
return $value;
}
$this->put($key, $value = $callback(), $minutes);
return $value;
}
/**
* Stores an item in the cache with an unlimited time-to-live.
*
* @param $key
* @param $item
* @return bool
*/
public function forever($key, $item)
{
return set_transient($this->prefix.$key, $item, 0);
}
/**
* Removes an item from the cache.
*
* @param $key
* @return bool
*/
public function forget($key)
{
return delete_transient($this->prefix.$key);
}
/**
* Flushes stale items out of cache.
*
* @param bool $hard if true, delete old transients rather than invalidate them by WordPress means.
*/
public function flush($hard = false)
{
global $wpdb;
$transients = $wpdb->get_col(
$wpdb->prepare("
SELECT REPLACE(option_name, '_transient_timeout_', '') AS transient_name
FROM {$wpdb->options}
WHERE option_name LIKE '\_transient\_timeout\__%%'
", array())
);
foreach ($transients as $transient) {
if ($hard === true) {
delete_transient($transient);
} else {
get_transient($transient);
}
}
}
public function getPrefix()
{
return $this->prefix;
}
}
<?php
namespace Vendor\Services;
use Mockery as m;
/**
* Set/Update the value of a transient;
*
* @param $transient
* @param $value
* @param $expiration
*
* @return boolean True if successful. False, otherwise.
*/
function set_transient($transient, $value, $expiration)
{
return CacheTest::$functions->set_transient($transient, $value, $expiration);
}
/**
* Get the value of a transient
*
* @param $transient
*
* @return mixed Value of transient if exists. False, otherwise.
*/
function get_transient($transient)
{
return CacheTest::$functions->get_transient($transient);
}
/**
* Delete a transient.
*
* @param $transient
*
* @return boolean True if successful. False, otherwise.
*/
function delete_transient($transient)
{
return CacheTest::$functions->delete_transient($transient);
}
class CacheTest extends \PHPUnit_Framework_TestCase
{
public static $functions;
public static $wpdb;
public $cache;
public function setUp()
{
global $wpdb;
self::$functions = m::mock();
self::$wpdb = $wpdb = m::mock();
$wpdb->options = '';
$this->cache = new Cache();
}
public function tearDown()
{
m::close();
}
/** @test */
public function it_works_and_has_default_prefix()
{
$this->assertNotEmpty($this->cache->getPrefix());
}
/** @test */
public function it_returns_value_if_exists_in_cache()
{
$key = $this->cache->getPrefix() . 'foo';
self::$functions->shouldReceive('get_transient')
->with($key)
->once()
->andReturn('bar');
$output = $this->cache->get('foo');
$this->assertEquals('bar', $output);
}
/** @test */
public function it_returns_null_if_does_not_exist_in_cache()
{
$key = $this->cache->getPrefix() . 'foo';
/*
* This mock covers the case that a transient doesn't exist,
* does not have a value, or has expired.
*/
self::$functions->shouldReceive('get_transient')
->with($key)
->once()
->andReturn(false);
$output = $this->cache->get('foo');
$this->assertNull($output);
}
/** @test */
public function it_can_tell_if_a_value_exists_for_cache_key()
{
$key = $this->cache->getPrefix() . 'foo';
self::$functions->shouldReceive('get_transient')
->with($key)
->once()
->andReturn(false);
$this->assertFalse($this->cache->has('foo'));
self::$functions->shouldReceive('get_transient')
->with($key)
->once()
->andReturn('bar');
$this->assertTrue($this->cache->has('foo'));
}
/** @test */
public function it_can_store_a_value_in_the_cache_with_an_expiration()
{
self::$functions->shouldReceive('set_transient')
->with($this->cache->getPrefix() . 'foo', 'bar', 300)
->once()
->andReturn(true);
$output = $this->cache->put('foo', 'bar', 5);
$this->assertTrue($output);
}
/** @test */
public function it_can_store_a_value_in_the_cache_with_default_expiration()
{
self::$functions->shouldReceive('set_transient')
->with($this->cache->getPrefix() . 'foo', 'bar', 60)
->once()
->andReturn(true);
$this->cache->put('foo', 'bar');
}
/** @test */
public function it_returns_false_when_cached_value_is_not_stored()
{
self::$functions->shouldReceive('set_transient')
->with($this->cache->getPrefix() . 'foo', 'bar', 300)
->once()
->andReturn(false);
$output = $this->cache->put('foo', 'bar', 5);
$this->assertFalse($output);
}
/** @test */
public function it_can_store_a_value_in_the_cache_forever()
{
self::$functions->shouldReceive('set_transient')
->with($this->cache->getPrefix() . 'foo', 'bar', 0)
->once()
->andReturn(true);
$output = $this->cache->forever('foo', 'bar');
$this->assertTrue($output);
}
/** @test */
public function it_can_manually_remove_a_cached_value()
{
self::$functions->shouldReceive('delete_transient')
->with($this->cache->getPrefix() . 'foo')
->once()
->andReturn(true);
$output = $this->cache->forget('foo');
$this->assertTrue($output);
}
/** @test */
public function it_returns_false_when_cached_value_could_not_be_removed()
{
self::$functions->shouldReceive('delete_transient')
->with($this->cache->getPrefix() . 'foo')
->once()
->andReturn(false);
$output = $this->cache->forget('foo');
$this->assertFalse($output);
}
/** @test */
public function it_can_remove_all_transients()
{
self::$wpdb->shouldReceive('prepare')
->times(2)
->andReturn('sql statement');
self::$wpdb->shouldReceive('get_col')
->times(2)
->andReturn(array($this->cache->getPrefix().'sample'));
self::$functions->shouldReceive('get_transient')
->with($this->cache->getPrefix().'sample')
->once()
->andReturn(true);
self::$functions->shouldReceive('delete_transient')
->with($this->cache->getPrefix().'sample')
->once()
->andReturn(true);
$this->cache->flush();
$this->cache->flush($hard = true);
}
}
@mdwheele
Copy link
Author

This strategy for mocking function calls requires that the test be in the same namespace as the SUT being exercised. Tested outside of WordPress environment; declaring proxies to mock objects allows you to set expectations on behaviour of the functions without need for WordPress.

The trade-off here is that you're now mocking behaviour of a system / code you don't control. Because of this, while it is a good strategy for isolating plugin development during development, developers should also write tests that cover actual integration with WordPress in an actual environment.

Anywho, there is room for improvement on readability for sure (when expressing mock expectations) but for all intents and purposes this has served me well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment