fmalk / codeigniter-phpunit

Hack to make CodeIgniter work with PHPUnit.
234 stars 61 forks source link

Unable to test Models #15

Closed MathieuNls closed 10 years ago

MathieuNls commented 10 years ago

Hi, Thanks for your hacks. However, I am not able to test the application model :x. phpunit crash without any messages when loading models...

I am using a brand new instance of CI 2.2 with your hacks inside.

The test suite:

#!php

class testSuiteTest extends PHPUnit_Framework_TestCase
{

    public function testEmailValidation()
    {
        $CI =& get_instance();
        $CI->load->helper('email');
        $this->assertTrue(valid_email('test@test.com'));
        $this->assertFalse(valid_email('test#test.com'));

    }

    public function testModel()
    {
        $CI =& get_instance();
        $CI->load->model('test_model');
        $this->assertTrue($CI->test_model->test());
    }
}

The model:

#!php

class test_model extends CI_Model
{

    public function test()
    {
        return true;
    }

}

The results of phpunit command result:

crash

If I comment out the model test, there is no problem:

crash2

And if a enter a wrong model name I will have a CI exception

#!php
  public function testModel()
    {
        $CI =& get_instance();
        $CI->load->model('blabla');
        $this->assertTrue($CI->test_model->test());
    }

crash3

Note that the statement leading to the crash is :

#!php

 $CI->load->model('test_model');

because it crashes whether or not the following assert is here.

Finally; the return code of phpunit is 255 and I have all my errors activated for php (e.g E_ALL, ...).

Any clues ? Thanks.

fmalk commented 10 years ago

@MathieuNls I'll take a look and get back to you

fmalk commented 10 years ago

Before I replicate your problem in my machine, are you sure this isn't because you named your class test_model when you should follow CI rule of capitalizing the first letter? It should be Test_model, file name /models/test_model.php, and loaded by $CI->load->model('Test_model')

MathieuNls commented 10 years ago

Hi Fernando,

Thanks for your answer. I changed my code as suggested, but the problem persists. However, I narrowed it down to the $this->load->database(); statement.

New test case extending CITestCase for the $this->CI. Note that extending PHPUnit_Framework_TestCase won't change the output.

class TestModelTest extends CITestCase
{

    public function setUp()
    {
        $this->CI->load->model('Test_model');
    }

    public function testFunctionInModel()
    {
        $CI =& get_instance();
        $this->assertEquals("Mathieu", $this->CI->Test_model->test());
    }
}

Model

class Test_model extends CI_Model
{
    public function __construct()
    {
        parent::__construct();
    }

    public function test()
    {
        /**
         * Comment us and we work like a charm. 
         * Commenting only $this->db->get... isn't working either
         */
        $this->load->database();
        $query = $this->db->get('phone_carrier');
        /**
         * Replace return by hardcoded return "Mathieu";
         * and it will work
         */
        return $query->row()->name;
    }
}

Default welcome controller that outputs Mathieu as expected:

class welcome extends CI_Controller
{
    public function index()
    {
        $this->load->model('Test_model');
        echo $this->Test_model->test();
    }
}

Database

CREATE TABLE IF NOT EXISTS `phone_carrier` (
  `name` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `phone_carrier` (`name`) VALUES
('Mathieu');

Output in console: bug

Note the dots on the console. Three assertions are made, the two from the default EmailHelperTest you provide plus the one I added in TestModelTest and phpunit exits without any warning or error.

Thanks for your help, Mathieu

fmalk commented 10 years ago

@MathieuNls well, I gotta tell you, testing a model making queries to a database is a nightmare. In fact, it isn't even considered unit testing, you're not testing your model in isolation, you rely on another system being available and populated. This is a complicated matter to discuss, how to properly unit test a Model who is coupled to a database system. In an ideal world, you model would get the database resource at runtime, that way, in a test, you could inject another fake/mock connection and test using a database in a known state. CodeIgniter 2.x doesn't enforce this dependency injection model of system design, it encourages you to use $this->load->database() as you do, so it's up to you to make a configuration load a completely different database when unit testing. I've created CITestCase with database testing in mind, but this project's code is just a stub, every project and owner wants to test their models against their database in different ways (some use fixtures in another real DB schema, some use in-memory DBs, etc), so it's really up to you to study how you wanna do that in your project. The place to start is http://phpunit.de/manual/current/en/phpunit-book.html#database. I hope this helps. It is a difficult topic but it will improve your architecture skill immensely.

MathieuNls commented 10 years ago

Hi Fernando,

Thanks for your answer and the pointers. I did my homework and come up with a solution that fit my needs. I'll put it here as I spent dozens of hours searching in vain... Maybe, you'll have some comments.

I wanted to test my website against a test database (which is basically a copy of the production database with minimum data) and be able to check the integrity of newly inserted rows.

MY_Model

First of all, I've created a MY_Model that loads the database in its constructor only if we are not in testing mode.

class MY_Model extends CI_Model
{
    public function __construct()
    {
        parent::__construct();
        // When loading the model, make sure the db class is loaded
        // Do it when we are not testing !
        if ( ! isset($this->db) && !defined(PHPUNIT_TEST)) {
            $this->load->database();
        }
    }
}

Actor_Model (a classical CI model)

<?php

class Actor_model extends MY_Model
{
    public function __construct()
    {
        parent::__construct();
    }

    public function dumb()
    {
        return "Mathieu";
    }

    public function getFirstActorName()
    {
        $query = $this->db->get('Actor');

        return $query->row()->Name;
    }

    public function countActor()
    {
        return $this->db->count_all_results('Actor');
    }

    public function getActorById($id)
    {
         return $this->db->get_where('Actor', array('id' => $id));
    }
}

TestCase

I leveraged the possibility to load different database configuration with $this->CI->load->database('default_test'); (load the $db['default_test'] group in application/config/database.php). The comments in the following excerpt should be enough to understand what happens.

class TestModelTest extends CITestCase
{

    protected $myModel;

    /**
     * Load the database and populate myModel
     */
    public function setUp()
    {
        $this->CI->load->model('Actor_model');
        $this->CI->load->database('default_test');
        $this->myModel = $this->CI->Actor_model;
    }

    /**
     * Test that your model is operational
     */
    public function testWithoutAnyDatabaseAccess()
    {
        $this->assertEquals(
            "Mathieu",
            $this->myModel->dumb()
        );
    }

    /**
     * Test Model's query against hardcoded data
     */
    public function getFirstActorName()
    {
        $this->assertEquals(
            "Mathieu",
            $this->myModel->getFirstActorName()
        );
    }

    /**
     * The model and the test case fetch the same information
     * If the model "corrupt" the data, the test will fail
     */
    public function testWithDatabaseAccessByModelAndTest()
    {
        $this->assertEquals(
            $this->getConnection()->getRowCount('Actor'),
            $this->myModel->countActor()
        );
    }

    /**
     * Assert XML dataset against test db
     * File generated with
     * mysqldump --xml -t -u root --password=password database Actor --where="id=1" >
     * /var/www/html/CodeIgniter/application/tests/actor.xml
     *
     * This can be use for to confirm insertion integrity... We can check that w/ hardcoded
     * results.
     */
    public function testDatabaseAgainstXML()
    {
        $expected = $this->createMySQLXMLDataSet(__DIR__.'/../actor.xml');

        // Do something w/ your model that modify the database.

        $actual = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
        $actual->addTable('Actor', 'Select * From Actor where id =1');
        $this->assertDataSetsEqual($expected, $actual);
    }

For the last one, I used mysqldump --xml -t -u root --password=password database table --where="id=1" to create the XML file, but they are simple enough to be created by hands if need be:

<?xml version="1.0"?>
<mysqldump xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<database name="databse">
    <table_data name="Actor">
    <row>
        <field name="ID">1</field>
        <field name="Name">Mathieu</field>
    </row>
    </table_data>
</database>
</mysqldump>

CITest

I've changed the getConnection method of CITest so that it used global values set in phpunit.xml. (Found the trick here)

    /**
     * Initialize database connection (same one used by CodeIgniter)
     *
     * @return PHPUnit_Extensions_Database_DB_IDatabaseConnection
     */
    final public function getConnection()
    {
       if ($this->conn === null) {
            if (self::$pdo == null) {
                self::$pdo = new PDO( $GLOBALS['DB_DSN'], $GLOBALS['DB_USER'], $GLOBALS['DB_PASSWD'] );
            }
            $this->conn = $this->createDefaultDBConnection(self::$pdo, $GLOBALS['DB_NAME']);
        }

        return $this->conn;
    }

phpunit.xml

<?xml version="1.0" encoding="UTF-8" ?>
<phpunit bootstrap="application/tests/bootstrap.php"
    colors="true" 
    stopOnFailure="false" >
    <testsuites>
        <testsuite name="TestSuite">
            <directory>application/tests</directory>
        </testsuite>
    </testsuites>
    <php>
        <const name="PHPUNIT_TEST" value="1" />
        <const name="PHPUNIT_CHARSET" value="UTF-8" />
        <server name="REMOTE_ADDR" value="0.0.0.0" />
        <var name="DB_DSN" value="mysql:host=localhost;dbname=database" />
        <var name="DB_USER" value="root" />
        <var name="DB_PASSWD" value="" />
        <var name="DB_NAME" value="database" />
    </php>
    <filter>
        <blacklist>
            <directory suffix=".php">system</directory>
            <!--directory suffix=".php">application/libraries</directory-->
        </blacklist>
    </filter>
</phpunit>

Results

bug

As you can see, I have a warning PHP Warning: PHP Startup: Unable to load dynamic library '/usr/lib/php5/20121212/pdo_mysql.so' - /usr/lib/php5/20121212/pdo_mysql.so: undefined symbol: pdo_parse_params in Unknown on line 0 but it doesn't affect the end result...

XAMPP OR LAMP?

Also, it's might be good to know that I was using XAMPP on XUbuntu and despite my efforts, phpunit wasn't accessing the mysql database. Consequently, I switch to a LAMP stack and everything is ok now.

Conclusion

Thanks a lot for your hack of the CI core and for your answers here. I hope my little adventure can help fellow CI developers.

Happy testing.

fmalk commented 10 years ago

@MathieuNls your CITestCase::getConnection() looks good, I could improve the project to use that configuration instead of the standard CI one I use now. It can help people get started testing against a copy database like you did. So this was a win-win question. You got your issue resolved, I got new ideas. Glad I could help.