views:

912

answers:

1

Hello,

I have been getting into Unit Testing with Zend Framework. I am getting used to the other things it provide but I am having a hard time understanding Mock Objects.

For this example, I am trying to use a Mock Object to test out my model.

<?php

class Twitter_Model_Twitter
{
    private $_twitter;

    /**
     * Make the options injectable.
     * __contruct($auth, $key)
     */
    public function __construct() 
    {
        $config      = new Zend_Config_Ini(APPLICATION_INI, APPLICATION_ENV);
        $key         = $config->encryption->salt;
        $iv_size     = mcrypt_get_iv_size(MCRYPT_XTEA, MCRYPT_MODE_ECB);
        $iv          = mcrypt_create_iv($iv_size, MCRYPT_RAND);
        $password    = mcrypt_decrypt(MCRYPT_XTEA, $key, $password, MCRYPT_MODE_ECB, $iv);

        $this->_twitter = new Zend_Service_Twitter($username, $password);
    }


    public function verifyCredentials()
    {
        return $this->_twitter->account->verifyCredentials();
    }

    public function friendsTimeline($params)
    {
        return $this->_twitter->status->friendsTimeline($params);
    }
}

For my unit test:

require_once ('../application/models/Twitter.php');
class Model_TwitterTest extends ControllerTestCase
{
    /**
     * @var Model_Twitter
     */
    protected $_twitter;

    public function testfriendsTimeline() 
    {
        $mockPosts = array('foo', 'bar');    

        //my understanding below is:
        //get a mock of Zend_Service_Twitter with the friendsTimeline method
        $twitterMock = $this->getMock('Zend_Service_Twitter', array('friendsTimeline'));
        /*
        line above will spit out an error:
        1) testfriendsTimeline(Model_TwitterTest)
        Missing argument 1 for Mock_Zend_Service_Twitter_9fe2aeaa::__construct(), called in 
        /Applications/MAMP/bin/php5/lib/php/PHPUnit/Framework/TestCase.php on line 672 and
        defined /htdocs/twitter/tests/application/models/TwitterTest.php:38
        */

        $twitterMock->expects($this->once())
              ->method('friendsTimeline')
              ->will($this->returnValue($mockPosts));

        $model = new Twitter_Model_Twitter();
        $model->setOption('twitter', $twitterMock);

        $posts = $model->friendsTimeline(array('count'=>20));
        $this->assertEquals($posts, $mockPosts);
    }
}

How would you test the following? 1) verifyCredentials() 2) friendsTimeline()

Thanks, Wenbert

A: 

I am going to answer this question. I think I have made this work thanks to zomg from #zftalk.

Here is my new Twitter Model:

<?php
//application/models/Twitter.php
class Twitter_Model_Twitter
{
    private $_twitter;
    private $_username;
    private $_password;

    public function __construct(array $options = null) 
    {        
        if (is_array($options)) {
            $this->setOptions($options);
            $this->_twitter = new Zend_Service_Twitter($this->_username, $this->_password);
        } else {
            $twitterAuth = new Zend_Session_Namespace('Twitter_Auth');
            $config      = new Zend_Config_Ini(APPLICATION_INI, APPLICATION_ENV);
            $key         = $config->encryption->salt;
            $iv_size     = mcrypt_get_iv_size(MCRYPT_XTEA, MCRYPT_MODE_ECB);
            $iv          = mcrypt_create_iv($iv_size, MCRYPT_RAND);
            $password    = mcrypt_decrypt(MCRYPT_XTEA, $key, $twitterAuth->password, MCRYPT_MODE_ECB, $iv);

            $username    = $twitterAuth->username;
            $this->_twitter = new Zend_Service_Twitter($username, $password);
        }
    }

    public function setOptions(array $options)
    {
        $methods = get_class_methods($this);
        foreach ($options as $key => $value) {
            $pieces = explode('_', $key);
            foreach($pieces AS $piece_key => $piece_value) {
                $pieces[$piece_key] = ucfirst($piece_value);
            }

            $name = implode('',$pieces);
            $method = 'set' . $name;
            //$method = 'set' . ucfirst($key);
            if (in_array($method, $methods)) {
                $this->$method($value);
            }
        }
        return $this;
    }

    //I added this method. So that I could "inject"/set the $_twitter obj
    public function setTwitter($obj)
    {
        $this->_twitter = $obj;
        return $this;
    }


    public function verifyCredentials()
    {
        return $this->_twitter->account->verifyCredentials();
    }

    public function friendsTimeline($params)
    {
        return $this->_twitter->status->friendsTimeline($params);
    }
    //in the real code, more will go here...
}

And in my Unit Test, I have this:

<?php
// tests/application/models/TwitterTest.php
require_once ('../application/models/Twitter.php');

class Model_TwitterTest extends ControllerTestCase
{
    public function testVerifyCredentials() 
    {
        $stub = $this->getMock('Zend_Service_Twitter', array('verifyCredentials'),array(),'',FALSE);
        //FALSE is actually the 5th parameter to flag getMock not to call the main class. See Docs for this.
        //Now that I have set the $_twitter variable to use the mock, it will not call the main class - Zend_Rest_Client (i think)

        $stub->expects($this->once())
              ->method('verifyCredentials');

        $model = new Twitter_Model_Twitter();

        //this is the part where i set the $_twitter variable in my model to use the $stub
        $model->setOptions(array('twitter'=>$stub)); 

        $model->verifyCredentials();
    }
}

Anyways, I think I got it working.

1) The unit test no longer tried to connect to twitter.com:80

2) After I got the setOptions() working in the Twitter_Model, $model->verifyCredentials() in my unit test was successfully called.

I will wait for others in Stackoverflow to confirm that is the right answer. For the meantime, would like to hear from you guys.

Thanks!!!

wenbert