Unable to test Models
MathieuNls opened this issue · 6 comments
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:
If I comment out the model test, there is no problem:
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());
}
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.
@MathieuNls I'll take a look and get back to you
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')
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');
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
@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.
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
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.
@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.