Welcome Guest, Not a member yet? Register   Sign In
Unit Testing & Mock Objects
#11

[eluser]Aniket[/eluser]
I even read that article too...its applicable with an Object Oriented Application. where in php functions are used for abstraction of logic or database handling. But with CI its a problem as the way CI works. we just get instance of database using a single line $this->load->database(). May be i need to look into the /system/Database directory and check if i can figure out a way around.
#12

[eluser]Aniket[/eluser]
:-) Finally after lot of hacking CI source code i was able to figure out how to create a Mock object :-). Will post my findings and code in couple of days. But right now i am too excited and trying to inject this mock object into the model and perform unit testing on it. :-)
#13

[eluser]Aniket[/eluser]
Well this what i tried and was successful.

Code:
<?php

require_once(BASEPATH.'database/DB'.EXT);

class Data{
var $db;
function __construct()
{
    $this->db = &DB;();
}

function test($str)
{
    $result = $this->db->query($str);
    return $result->num_rows();
}
}

Mock::generate('Data');
class TestMockClass extends UnitTestCase{

function __construct()
{
    $this->UnitTestCase("Mock object demo");
}
function setUp()
{
$this->db= & new MockData($this);
}
function tearDown()
{
unset($this->db);
}

function testActor()
{
   $this->db->setReturnValue('test', 3);
   $str ="select * from user";
   $var = $this->db->test($str);
   $this->assertEqual($var,3);
}
}

But i had couple of problems:
1) I had to make changes to the database configuration to point to the desired database.
2) I could not instantiate the db's instance from the CI's super object so i decide to include the DB file and get the instance.

Haven't got much time to think over it. If anybody finds a solution please post it.

Thanks in advance
#14

[eluser]Unknown[/eluser]
Thanks Aniket!

This will work well for some tests but I wish they had an easier way to hook in to the database. I feel like the only way to truly be able to run unit tests you would have to create your own CodeIgniter driver that emulates the necessary functionality. The issue I am running into is I have a model:
Code:
class User extends Model {
  function getUser($id) {
    $query = $this->db->get_where('users', array('id' => $id));
    if ($query->num_rows() == 0) return null;
    return $query->row();
  }
}

You should be able to test that model from a unit test by calling it directly from within the test:
Code:
$user = $ci->user->getUser(1);
$this->assertNotNull($user);

So your Data class would have to have the get_where function and the other functions within CI in order to handle that (I think).

Or am I wrong by design? In the MVC framework for ASP.NET there is an additional layer so you have controller->service->model->database instead of controller->model->database here (although CI's drivers may work as a service layer)

I guess an alternative would be to have the User model just interface against another class (IUser) and make two versions that implement it - one that touches the database and one that doesn't, but I feel like that would be pointless unless you did Integration Testing as well..

AHHH! I'm going back to BASIC.
#15

[eluser]ch5i[/eluser]
Hello,

after a lot of trial & error I got this finally to work.
Thanks for your post Aniket, that got me started.

So, here it is: A simple example on how to mock the CI database class.


User model (test subject)
Code:
<?php

/**
* User_model
*/
class User_model extends Model {

    function User_model()
    {
        parent::__construct();
    }


    public function getAllUsers()
    {    
        return $this->db->get('users');
    }


    public function add($firstname)
    {    
        $usr = new stdClass();
        $usr->firstname = $firstname;
        return $this->db->insert('users', $usr);
    }

}


Unit Test Case
Code:
<?php if (!defined('BASEPATH')) exit('No direct script access allowed');


class TestMockClass extends UnitTestCase{
    
    // holds reference to ci instance
    private    $_ci = NULL;
    
    
    public function __construct()
    {
        $this->UnitTestCase("Mock object demo 2");
        
        // fetch ref to ci instance
        $this->_ci =& get_instance();
        
        // generate a class that has the same interface as
        // CI's currently loaded db
        Mock::generate(get_class($this->_ci->db), 'fake_DB');
        
        // generate a class with the same interface as CI_DB_result
        require_once(BASEPATH.'database/DB_result'.EXT);
        Mock::generate('CI_DB_result', 'fake_DB_result');
        
        // load the test subject (user model)
        $this->_ci->load->model('user_model');
    }
    
    
    public function setUp()
    {
        // replace CI's db with the faked one
        $this->_ci->db = new fake_DB();
        
        // THIS MAKES IT WORK
        // explicitly replace the db object
        // in the scope of the test subject
        // to get the references working...
        $this->_ci->user_model->db = $this->_ci->db;
    }
    
    
    public function tearDown()
    {    
        // not needed
        // unset($this->_ci->db);
    }
    
    
    public function testAddNewUser()
    {    
        // the 'add' method in user_model calls
        // the 'insert' method of CI's db object ($this->db->insert()),
        // which we replaced with the fake (mocked) one.
        // It has the same interface, but no implementation for the methods
        // so we have to tell it what to return for a given method call
        //
        // array('users', '*') means first param of insert must
        // be 'users' (the tablename), the second can be whatever
        $this->_ci->db->setReturnValue(
                                        'insert',
                                        TRUE,
                                        array('users', '*')
                                       );

        $this->assertTrue($this->_ci->user_model->add('Jack'), '%s');
    }
        
    
    public function testGetDbResult()
    {
        // create some dummy data to return later
        $jim = new stdClass();
        $jim->firstname = 'Jim';
        $jim->id        = 1;
    
        $joe = new stdClass();
        $joe->firstname = 'Joe';
        $joe->id        = 4;
        
        $result = array($jim, $joe);
                
        // set the query to mock ci_db_result
        // we want to get a mock db_result
        // where we can control what comes out of our
        // 'virtual' database
        $query = new fake_DB_result();

        // set the return value for the num_rows() method
        $query->setReturnValue('num_rows', 2);
        
        // set the return value for the row(2) method call
        $query->setReturnReference('row', $joe, array(2));
        
        // set the return value for the result() method call
        $query->setReturnReference('result', $result);
        
        // set the return value for db->get('users') method call
        $this->_ci->db->setReturnReference(
                                            'get',
                                            $query,
                                            array('users')
                                           );

        // implicitly call the db->get() method (in user_model)
        $returned_query = $this->_ci->user_model->getAllUsers();
        $this->assertReference($query, $returned_query);
        
        // call num_rows() on the returned (fake) db_result
        $this->assertEqual($returned_query->num_rows(), 2);
        
        // call row(2) on the returned (fake) db_result
        $this->assertEqual($returned_query->row(2)->firstname, 'Joe');
        
        // call result() method on the returned (fake) db_result
        foreach($returned_query->result() as $user) {
            $this->assertPattern('#J.{2}#', $user->firstname);
        }
        
    }
}

Sidenote:
I use SimpleTest, set up as described in the Wiki.
Well, almost as described, I do not autoload the simpletester library (step 5), but have a unit test controller which loads it and calls the Run() method, so that the tests are not run everytime i call a controller.
#16

[eluser]Unknown[/eluser]
@Aniket @ch5i
thanks guys, you really helped me with mock object
by the way, I got an error saying "Call to a member function result() on a non-object" in my model which is:
Code:
return $query->result();

anyone know why?
thanks

anyway, here's my test case:
Code:
<?php

class test_mustahik_model extends CodeIgniterUnitTestCase
{
    protected $rand = '';
    // holds reference to ci instance
    protected $_ci = NULL;

    public function __construct()
    {
        parent::__construct('mustahik Model');

        // fetch ref to ci instance
        $this->_ci =& get_instance();
        
        // generate a class that has the same interface as
        // CI's currently loaded db
        Mock::generate(get_class($this->_ci->db), 'fake_DB');
        
        // generate a class with the same interface as CI_DB_result
        require_once(BASEPATH.'database/DB_result'.EXT);
        Mock::generate('CI_DB_result', 'fake_DB_result');
        
        // load the test subject (mustahik model)
        $this->_ci->load->model('mustahik_model');
    }

    public function setUp()
    {
        // replace CI's db with the faked one
        $this->_ci->db = new fake_DB();
        
        // THIS MAKES IT WORK
        // explicitly replace the db object
        // in the scope of the test subject
        // to get the references working...
        $this->_ci->mustahik_model->db = $this->_ci->db;
    }

    public function tearDown()
    {
        // not needed
        // unset($this->_ci->db);
    }
    
    public function test_getAll() {
        $all = $this->_ci->mustahik_model->getAll();
        $this->assertIsA($all, 'array');
    }

}

/* End of file test_mustahik_model.php */
/* Location: ./tests/models/test_mustahik_model.php */
?>




Theme © iAndrew 2016 - Forum software by © MyBB