Mocking abstract classes with SimpleTest

Discussion of testing theory and practice, including methodologies (such as TDD, BDD, DDD, Agile, XP) and software - anything to do with testing goes here. (Formerly "The Testing Side of Development")

Moderator: General Moderators

Post Reply
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Mocking abstract classes with SimpleTest

Post by Chris Corbyn »

Error wrote:Fatal error: Declaration of PartialSmtpConnectionIO::write() must be compatible with that of Swift_Connection::write() in /Users/d11wtq/PHPLibs/simpletest/mock_objects.php(1001) : eval()'d code on line 134
I'm trying to Mock this class:

Code: Select all

<?php

/**
 * Swift Mailer Connection Base
 * All connection handlers extend this abstract class
 * Please read the LICENSE file
 * @author Chris Corbyn <chris@w3style.co.uk>
 * @package Swift_Connection
 * @license GNU Lesser General Public License
 */

/**
 * Swift Connection Base class
 * Lists methods which are required by any connections as well as providing some common functions.
 * @package Swift_Connection
 * @author Chris Corbyn <chris@w3style.co.uk>
 */
abstract class Swift_Connection
{
  /**
   * Any extensions the server might support
   * @var array
   */
  protected $extensions = array();
  /**
   * True if the connection is ESMTP.
   * @var boolean
   */
  protected $isESMTP = false;
  
  /**
   * Set an extension which the connection reports to support
   * @param string Extension name
   * @param array Attributes of the extension
   */
  public function setExtension($name, $options=array())
  {
    $this->extensions[$name] = $options;
  }
  /**
   * Check if a given extension has been set as available
   * @param string The name of the extension
   * @return boolean
   */
  public function hasExtension($name)
  {
    return array_key_exists($name, $this->extensions);
  }
  /**
   * Execute any needed logic after connecting and handshaking
   */
  public function postConnect(Swift $instance) {}
  /**
   * Get the list of attributes supported by the given extension
   * @param string The name of the connection
   * @return array The list of attributes
   * @throws Swift_ConnectionException If the extension cannot be found
   */
  public function getAttributes($extension)
  {
    if ($this->hasExtension($extension))
    {
      return $this->extensions[$extension];
    }
    else
    {
      throw new Swift_ConnectionException(
      "Unable to locate any attributes for the extension '" . $extension . "' since the extension cannot be found. " .
      "Consider using hasExtension() to check.");
    }
  }
  /**
   * Returns TRUE if the connection needs a EHLO greeting.
   * @return boolean
   */
  public function getRequiresEHLO()
  {
    return $this->isESMTP;
  }
  /**
   * Set TRUE if the connection needs a EHLO greeting.
   * @param boolean
   */
  public function setRequiresEHLO($set)
  {
    $this->isESMTP = (bool) $set;
  }
  /**
   * Try to start the connection
   * @throws Swift_ConnectionException If the connection cannot be started
   */
  abstract public function start();
  /**
   * Return the contents of the buffer
   * @return string
   * @throws Swift_ConnectionException If the buffer cannot be read
   */
  abstract public function read();
  /**
   * Write a command to the buffer
   * @param string The command to send
   * @throws Swift_ConnectionException If the write fails
   */
  abstract public function write($command, $end="\r\n");
  /**
   * Try to stop the connection
   * @throws Swift_ConnectionException If the connection cannot be closed/stopped
   */
  abstract public function stop();
  /**
   * Check if the connection is up or not
   * @return boolean
   */
  abstract public function isAlive();
}
I've faced this problem with this same class before. My resolution was to create an interface listing all my abstract methods, then have the abstract implement the interface, finally mocking the abstract as usual. But I'd say this is more of an issue with mock objects than a good coding practise because it's a dirty hack I'm now trying to reverse for the second time. There's no point at all listing an interface twice. Besides, PHP should still throw the same error if the interface (which is concrete) is not implemented correctly no matter how many times it's bee subclassed.

Does anyone know how I can mock this class? I need to make expectations on values passed to write() so I cannot use a partial and exclude this method neither :(
User avatar
Ambush Commander
DevNet Master
Posts: 3698
Joined: Mon Oct 25, 2004 9:29 pm
Location: New Jersey, US

Post by Ambush Commander »

Try this patch to SimpleTest:

Open up reflection_php5.php, find the getSignature() function. Replace function contents with this:

Code: Select all

/**
         *    Gets the source code matching the declaration
         *    of a method.
         *    @param string $name    Method name.
         *    @return string         Method signature up to last
         *                           bracket.
         *    @access public
         */
        function getSignature($name) {
         // hacked up by Edward: replace most "function $name" with
         // full signature to deal with E_STRICT warnings
        	if ($name == '__set') {
        		return 'function __set($key, $value)';
        	}
        	if ($name == '__call') {
        		return 'function __call($method, $arguments)';
        	}
            if (version_compare(phpversion(), '5.1.0', '>=')) {
                if (in_array($name, array('__get', '__isset', $name == '__unset'))) {
                    return "function {$name}(\$key)";
                }
            }
            $reflector = new ReflectionMethod($this->_interface, $name);
            if (method_exists($this->_interface, $name) || 
                method_exists($this->_interface, '__call')) {
                if ($reflector->isStatic()) {
                    return "static " . $this->_getFullSignature($name);
                } else {
                	return $this->_getFullSignature($name);
                }
            }
        	//if ($this->_isInterfaceMethod($name)) {
        	    return $this->_getFullSignature($name);
        	//}
         //   return "function $name()";
        }
User avatar
Jenk
DevNet Master
Posts: 3587
Joined: Mon Sep 19, 2005 6:24 am
Location: London

Post by Jenk »

Code: Select all

if (in_array($name, array('__get', '__isset', $name == '__unset'))) {
Shouldn't that be:

Code: Select all

if (in_array($name, array('__get', '__isset', '__unset'))) {
User avatar
Ambush Commander
DevNet Master
Posts: 3698
Joined: Mon Oct 25, 2004 9:29 pm
Location: New Jersey, US

Post by Ambush Commander »

Yes. Not my fault though: that code is in the CVS. We should tell them about it.

Oh, and it looks like they fixed it (the abstract class problem) in the latest revision. Regular mocks will still emit E_STRICT errors though...
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Ambush Commander wrote:Yes. Not my fault though: that code is in the CVS. We should tell them about it.

Oh, and it looks like they fixed it (the abstract class problem) in the latest revision. Regular mocks will still emit E_STRICT errors though...
Awesome, I'll switch to the CVS head :) Won't be able to test until later however.
User avatar
Ambush Commander
DevNet Master
Posts: 3698
Joined: Mon Oct 25, 2004 9:29 pm
Location: New Jersey, US

Post by Ambush Commander »

Word of warning: the CVS head is occasionally a bit unstable. As of right now, it seems alright though.
Post Reply