diff --git a/ctools.info b/ctools.info
index 045b083..05f4702 100644
--- a/ctools.info
+++ b/ctools.info
@@ -6,4 +6,12 @@ files[] = includes/context.inc
 files[] = includes/css-cache.inc
 files[] = includes/math-expr.inc
 files[] = includes/stylizer.inc
+
+files[] = tests/context.test
+files[] = tests/css.test
 files[] = tests/css_cache.test
+files[] = tests/ctools.plugins.test
+files[] = tests/math_expression.test
+files[] = tests/math_expression_stack.test
+files[] = tests/object_cache.test
+files[] = tests/page_tokens.test
diff --git a/ctools.module b/ctools.module
index 78ac26b..43761da 100644
--- a/ctools.module
+++ b/ctools.module
@@ -314,23 +314,29 @@ function ctools_break_phrase($str) {
  * Set a token/value pair to be replaced later in the request, specifically in
  * ctools_page_token_processing().
  *
- * @param $token
+ * @param string $token
  *   The token to be replaced later, during page rendering.  This should
- *    ideally be a string inside of an HTML comment, so that if there is
- *    no replacement, the token will not render on the page.
- * @param $type
+ *   ideally be a string inside of an HTML comment, so that if there is
+ *   no replacement, the token will not render on the page.
+ *   If $token is NULL, the token set is not changed, but is still
+ *   returned.
+ * @param string $type
  *   The type of the token. Can be either 'variable', which will pull data
- *   directly from the page variables
- * @param $argument
- *   If $type == 'variable' then argument should be the key to fetch from
- *   the $variables. If $type == 'callback' then it should either be the
- *   callback, or an array that will be sent to call_user_func_array().
- *
- * @return
+ *   directly from the page variables, or 'callback', which causes a function
+ *   to be called to calculate the value. No other values are supported.
+ * @param string|array $argument
+ *   For $type of:
+ *   - 'variable': argument should be the key to fetch from the $variables.
+ *   - 'callback': then it should either be the callback function name as a
+ *     string, or an array that will be sent to call_user_func_array(). Argument
+ *     arrays must not use array keys (i.e. $a[0] is the first and $a[1] the
+ *     second element, etc.)
+ *
+ * @return array
  *   A array of token/variable names to be replaced.
  */
 function ctools_set_page_token($token = NULL, $type = NULL, $argument = NULL) {
-  static $tokens = array();
+  $tokens = &drupal_static('ctools_set_page_token', array());
 
   if (isset($token)) {
     $tokens[$token] = array($type, $argument);
@@ -339,13 +345,23 @@ function ctools_set_page_token($token = NULL, $type = NULL, $argument = NULL) {
 }
 
 /**
- * Easily set a token from the page variables.
+ * Reset the defined page tokens.
+ *
+ * Introduced for simpletest purposes. Normally not needed.
+ */
+function ctools_reset_page_tokens() {
+  drupal_static_reset('ctools_set_page_token');
+}
+
+/**
+ * Set a replacement token from the containing element's children during #post_render.
  *
  * This function can be used like this:
- * $token = ctools_set_variable_token('tabs');
+ *   $token = ctools_set_variable_token('tabs');
  *
- * $token will then be a simple replacement for the 'tabs' about of the
- * variables available in the page template.
+ * The token "<!-- ctools-page-tabs -->" would then be replaced by the value of
+ * this element's (sibling) render array key 'tabs' during post-render (or be
+ * deleted if there was no such key by that point).
  */
 function ctools_set_variable_token($token) {
   $string = '<!-- ctools-page-' . $token . ' -->';
@@ -354,10 +370,32 @@ function ctools_set_variable_token($token) {
 }
 
 /**
- * Easily set a token from the page variables.
+ * Set a replacement token from the value of a function during #post_render.
  *
  * This function can be used like this:
- * $token = ctools_set_variable_token('id', 'mymodule_myfunction');
+ *   $token = ctools_set_callback_token('id', 'mymodule_myfunction');
+ *
+ * Or this (from its use in ctools_page_title_content_type_render):
+ *   $token = ctools_set_callback_token('title', array(
+ *       'ctools_page_title_content_type_token', $conf['markup'], $conf['id'], $conf['class']
+ *     )
+ *   );
+ *
+ * The token (e.g: "<!-- ctools-page-id-1b7f84d1c8851290cc342631ac663053 -->")
+ * would then be replaced during post-render by the return value of:
+ *
+ *   ctools_page_title_content_type_token($value_markup, $value_id, $value_class);
+ *
+ * @param string $token
+ *   The token string for the page callback, e.g. 'title'
+ *
+ * @param string|array $callback
+ *   For callback functions that require no args, the name of the function as a
+ *   string; otherwise an array of two or more elements: the function name
+ *   followed by one or more function arguments.
+ *
+ * @return string
+ *   The constructed token.
  */
 function ctools_set_callback_token($token, $callback) {
   // If the callback uses arguments they are considered in the token.
diff --git a/tests/css_cache.test b/tests/css_cache.test
index e289b42..cf25f74 100644
--- a/tests/css_cache.test
+++ b/tests/css_cache.test
@@ -7,7 +7,7 @@
 /**
  * Tests the custom CSS cache handler.
  */
-class CtoolsObjectCache extends DrupalWebTestCase {
+class CtoolsCSSObjectCache extends DrupalWebTestCase {
 
   /**
    * {@inheritdoc}
diff --git a/tests/object_cache.test b/tests/object_cache.test
index 8791d7e..dd88c5a 100644
--- a/tests/object_cache.test
+++ b/tests/object_cache.test
@@ -5,6 +5,129 @@
  */
 
 /**
+ * Test ctools_cache_find_plugin and the structure of the default cache plugins.
+ */
+class CtoolsUnitObjectCachePlugins extends DrupalUnitTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Ctools Unit object cache storage',
+      'description' => 'Verify that objects are written, readable and lockable.',
+      'group' => 'ctools',
+    );
+  }
+
+  public function setUp() {
+    // Additionally enable ctools module.
+    parent::setUp('ctools');
+    ctools_include('cache');
+  }
+
+  /**
+   * Check that the supplied array looks like a ctools cache plugin.
+   *
+   * @param mixed $p the value to check.
+   * @param string $msg prefix of the assertion message.
+   */
+  public function assertValidCachePlugin($p, $msg) {
+    //$this->pass(var_export($p, TRUE));
+    $this->assertTrue(is_array($p), $msg . ': plugin is an array');
+
+    $this->assertEqual($p['plugin type'], 'cache', $msg . ': type is cache');
+
+    $this->assertTrue(array_key_exists('title', $p), $msg . ': title element exists');
+    $this->assertTrue(!empty($p['title']) && is_string($p['title']), $msg . ': title is a string');
+
+    $this->assertTrue(array_key_exists('cache get', $p), $msg . ': has a get function');
+    $this->assertTrue(!empty($p['cache get']) && is_callable($p['cache get']), $msg . ': get is executable');
+
+    $this->assertTrue(array_key_exists('cache set', $p), $msg . ': has a set function');
+    $this->assertTrue(!empty($p['cache set']) && is_callable($p['cache set']), $msg . ': set is executable');
+
+    // TODO: clear is required by the spec (cache.inc:40..48): but export_ui cache doesn't implement it.
+    $this->assertTrue(array_key_exists('cache clear', $p), $msg . ': has a clear function');
+    $this->assertTrue(is_callable($p['cache clear']), $msg . ': clear is executable');
+
+    // TODO: break is optional acc'd to spec but does anything implement it?
+    $this->assertTrue(!array_key_exists('cache break', $p) || is_callable($p['cache break']), $msg . ': break is executable');
+
+    // TODO: ?? finalize is optional so don't fail if not there??
+    $this->assertTrue(!array_key_exists('cache finalize', $p) || is_callable($p['cache finalize']), $msg . ': finalize is executable');
+  }
+
+  /**
+   * Check the return value of the ctools_cache_find_plugin function.
+   *
+   * @param mixed $p the value to check.
+   * @param string $msg prefix of the assertion message.
+   */
+  public function assertPluginNotFound($p, $msg) {
+    //$this->pass(var_export($p, TRUE));
+    $this->assertTrue(is_array($p), $msg . ': is an array');
+    $plugin = array_shift($p);
+    $this->assertNull($plugin, $msg . ': no plugin info');
+    $data = array_shift($p);
+    $this->assertTrue(empty($data) || is_string($data), $msg . ': data string-like');
+    $this->assertTrue(empty($p), $msg . ': just two elements');
+  }
+
+  /**
+   * Check the return value of the ctools_cache_find_plugin function.
+   *
+   * @param mixed $p the value to check.
+   * @param string $msg prefix of the assertion message.
+   */
+  public function assertPluginFound($p, $msg) {
+    //$this->pass(var_export($p, TRUE));
+    $this->assertTrue(is_array($p), $msg . ': is an array');
+    $plugin = array_shift($p);
+    $this->assertTrue(is_array($plugin), $msg . ': has plugin data');
+    $data = array_shift($p);
+    $this->assertTrue(empty($data) || is_string($data), $msg . ': data is string-like');
+    $this->assertTrue(empty($p), $msg . ': just two elements');
+  }
+
+  /**
+   * Test to see that we can find the standard simple plugin.
+   */
+  public function testFindSimpleCachePlugin() {
+    // The simple plugin.
+    $plugin = ctools_cache_find_plugin('simple');
+    $this->assertPluginFound($plugin, 'The Simple Cache plugin is present');
+    $this->assertValidCachePlugin($plugin[0], 'The Simple Cache plugin');
+
+    // The simple plugin, with ::.
+    $plugin = ctools_cache_find_plugin('simple::data');
+    $this->assertPluginFound($plugin, 'The Simple Cache plugin is present, with data');
+  }
+
+  /**
+   * Test to see that we can find the standard export_ui plugin.
+   */
+  public function testFindExportUICachePlugin() {
+    // The export plugin.
+    $plugin = ctools_cache_find_plugin('export_ui');
+    $this->assertPluginFound($plugin, 'The Export UI Cache plugin is present');
+    $this->assertValidCachePlugin($plugin[0], 'The Export Cache plugin');
+
+    // The export plugin, with ::.
+    $plugin = ctools_cache_find_plugin('export_ui::data');
+    $this->assertTrue(is_array($plugin), 'The Export UI Cache plugin is present, with data');
+  }
+
+  /**
+   * Test to see that we don't find plugins that aren't there.
+   */
+  public function testFindFoobarbazCachePlugin() {
+    // An imaginary foobarbaz plugin.
+    $plugin = ctools_cache_find_plugin('foobarbaz');
+    $this->assertPluginNotFound($plugin, 'The Foobarbaz Cache plugin is absent');
+    $plugin = ctools_cache_find_plugin('foobarbaz::data');
+    $this->assertPluginNotFound($plugin, 'The Foobarbaz Cache plugin is absent, with data');
+  }
+
+}
+
+/**
  * Test object cache storage.
  */
 class CtoolsObjectCache extends DrupalWebTestCase {
@@ -12,7 +135,7 @@ class CtoolsObjectCache extends DrupalWebTestCase {
     return array(
       'name' => 'Ctools object cache storage',
       'description' => 'Verify that objects are written, readable and lockable.',
-      'group' => 'Chaos Tools Suite',
+      'group' => 'ctools',
     );
   }
 
diff --git a/tests/page_tokens.test b/tests/page_tokens.test
new file mode 100644
index 0000000..e5767b9
--- /dev/null
+++ b/tests/page_tokens.test
@@ -0,0 +1,51 @@
+<?php
+/**
+ * @file
+ * Tests for different parts of the ctools object caching system.
+ */
+
+/**
+ * Test ctools page token processing.
+ *
+ * Needs to be a WebTest because need access to database tables.
+ */
+class CtoolsPageTokens extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Ctools Unit page token processing',
+      'description' => 'Verify that page tokens can be set and read.',
+      'group' => 'ctools',
+    );
+  }
+
+  public function setUp() {
+    // Additionally enable ctools module.
+    parent::setUp('ctools');
+
+    // Start each test anew.
+    ctools_reset_page_tokens();
+  }
+
+  /**
+   * Test that we can set page tokens.
+   */
+  public function testSetPageToken() {
+    ctools_set_page_token('id', 'variable', 'test');
+
+    $tokens = ctools_set_page_token();
+    $this->assertTrue(is_array($tokens) && count($tokens) === 1, 'Page tokens no longer empty.');
+    $this->assertEqual($tokens['id'], ['variable', 'test'], 'Page token has correct value.');
+  }
+
+  /**
+   * Test that we can set page tokens.
+   */
+  public function testSetVariableToken() {
+    $string = ctools_set_variable_token('title');
+    $tokens = ctools_set_page_token();
+
+    $this->assertTrue(is_array($tokens) && count($tokens) === 1, 'Page tokens no longer empty');
+    $this->assertEqual($tokens[$string], ['variable', 'title'], 'Page tokens no longer empty');
+  }
+
+}
