User Rating: 5 / 5

Star ActiveStar ActiveStar ActiveStar ActiveStar Active
 

Test Driven Development is very important to improve quality and reduces bugs in your code. In this article I will show you how to use TDD in developing Joomla component. First you have to install phpunit, you can find the details at here. In this article we will use Joomla article manager (com_content) component as an experiment, and this article is based on Joomla 3.2.0. Here are the steps:

1. Create a Test folder

Create a new folder called Tests on /joomlapath/administrator/components/com_content folder. This folder will contain all test files.

2. Create phpunit configuration file

Create a new file called phpunit.xml. we will use the following configurations for this experiment: 

<phpunit bootstrap="bootstrap.php"
	colors="true"
	convertErrorsToExceptions="true"
	convertNoticesToExceptions="true"
	convertWarningsToExceptions="true"
	processIsolation="false"
	stopOnFailure="false"
	syntaxCheck="false"
	verbose="true">
</phpunit>

3. Create a bootstrap file

Create a new file called bootstrap.php  in our Tests folder, we will use this bootstrap file to load the Joomla libraries so that we can run the component from our unit testing code. This bootstrap file is based on index.php file on administrator folder. We have to modify some codes so that it will work on our Tests folder. 

<?php
error_reporting(E_ALL);

define('_JEXEC', 1);
define('BASEPATH',realpath(dirname(__FILE__).'/../'));
define('JOOMLA_PATH',realpath(dirname(__FILE__).'/../../../../'));
define('JOOMLA_ADMIN_PATH',realpath(dirname(__FILE__).'/../../../'));
$_SERVER['HTTP_HOST'] = 'localhost';
$_SERVER['REQUEST_METHOD'] = 'GET';

if (file_exists(JOOMLA_ADMIN_PATH . '/defines.php'))
{
	include_once JOOMLA_ADMIN_PATH . '/defines.php';
}

if (!defined('_JDEFINES'))
{
	define('JPATH_BASE', JOOMLA_ADMIN_PATH);	
	require_once JPATH_BASE . '/includes/defines.php';
}

require_once JPATH_BASE . '/includes/framework.php';
require_once JPATH_BASE . '/includes/helper.php';
require_once JPATH_BASE . '/includes/toolbar.php';
define('JPATH_COMPONENT',JOOMLA_ADMIN_PATH.'/components/com_content');
$app = JFactory::getApplication('administrator');
include BASEPATH.'/controller.php';		
  • we should define _JEXEC in our bootstrap file so that we can use all php files on our Joomla installation because most Joomla files are protected from direct access using defined('_JEXEC') or die; code
  • we should create fake $_SERVER['HTTP_HOST'] array and set the value to localhost because it's used on /joomlapath/libraries/joomla/application/web.php on function detectRequestUri() and on /joomlapath/libraries/joomla/uri/uri.php on function getInstance()
  • we should define JPATH_COMPONENT because its used on /joomlapath/libraries/legacy/controller/legacy.php on class constructor
  • we should instantiated Joomla administrator application using JFactory::getApplication('administrator') before we can use it on our unit test functions
  • we should include the controller file so that we can use ContentController class in our unit test

4. Test PHPUnit configuration and bootstrap file from command line

Open your command line or terminal and navigate to /joomlapath/administrator/components/com_content/Tests folder and type phpunit . [enter] and you should see this output.

PHPUnit configuration test 

If everything is working you will see phpunit output : No tests executed! since we haven't created any test function.

5. Create a test file

Create a new file called ArticleTest.php, in this file we will test the functionality of article manager on the Joomla backend. As you can see on /joomlapath/administrator/components/com_content/models folder, Joomla com_content component has a model called articles. So in this experiment we will try to list all articles in your Joomla site. You can see it from testListArticles function below. 

<?php
class ArticleTest extends \PHPUnit_Framework_TestCase {
		
	public function testListArticles(){		
		$c = new ContentController();		
		$model = $c->getModel('articles');
		$articles = $model->getItems();
		var_dump($articles);
		$this->assertNotEmpty($articles);
	}
}

as you can see from the code, first it instantiated the ContentController class and get the model ContentModelArticles using getModel function and then it gets all articles using getItems function. You should see the following output from phpunit.

PHPUnit list all articles test

6. Simulate HTTP request from unit test

You can simulate HTTP request using JInput class, for example we want to test the search functionality of article manager. We can set the value of filter_search property in JInput class using set function. Here is the complete function. 

public function testSearchArticles(){
	//simulate user request: searching for 'contact'
	$input = JFactory::getApplication()->input; 
	$input->set('filter_search', 'contact');
		
	$c = new ContentController();		
	$model = $c->getModel('articles');				
	$articles = $model->getItems();
	var_dump($articles);
	$this->assertNotEmpty($articles);
}

In the above example we simulate the search article process by setting the value of filter_search property to 'contact'.

Update January 11, 2014

7. Create, Update, Delete Document Test

Let's continue to create a test for document saving process. We can see from Article Manager: Add New Article page, there are some fields that need to be filled which are Title and Category and these field name are jform[title] and jform[catid], you can use firebug to see the field name. And then lest create a new test function: testAddDocument :

public function testAddDocument(){
	$input = JFactory::getApplication()->input; 
	$input->set('id',0);		
	$input->set('task','article.save');		
	$data = array(			
		'title'=>'test document',//required			
		'catid'=>2,//uncategorised
		);
	$input->post->set('jform',$data);			
	$c = new ContentControllerArticle();		
	$this->assertEquals(false,$c->save());	
}

If you run phpunit you will get this output: ,

<script>document.location.href='http://localhost/usr/local/bin/index.php';</script>

Its happen when we try to create a new article without login to Joomla system, so Joomla can't find a user in the active session. Joomla also checks the token of the posted form using JSession::checkToken(). Let's add this code to our bootstrap file to register a user in joomla session and by pass JSession::checkToken() validation .

//initialize session and user
$session = JFactory::getSession();
$session->set('user', JUser::getInstance('admin'));
//bypass JSession::checkToken()
JFactory::getApplication()->input->post->set(JSession::getFormToken(),'1');

 run phpunit again and you'll get this error:

There was 1 error:
1) ArticleTest::testAddDocument
Use of undefined constant JPATH_COMPONENT_ADMINISTRATOR - assumed 'JPATH_COMPONENT_ADMINISTRATOR'

Lets fix this by adding this code to our bootstrap file

define('JPATH_COMPONENT_ADMINISTRATOR',JPATH_COMPONENT);

let's run phpunit again and you will get all tests passed. Now lets create a test function for update article process: testUpdateArticle. This new function will depends on the previous testAddDocument function, that's why we need to modify postSaveHook function of ContentControllerArticle class in order to track the last added article.

private $savedArticle;
protected function postSaveHook(JModelLegacy $model, $validData = array())
{
	$this->savedArticle = $model->getItem();
	return;
}
public function getSavedArticle()
{
	return $this->savedArticle;
}

and our testAddArticle function will become:

public function testAddArticle(){
	$input = JFactory::getApplication()->input;
	$input->set('id',0);    
	$input->set('task','article.save');     
	$data = array(         
		'title'=>'test document',//required         
		'catid'=>2,//uncategorised
	);
	$input->post->set('jform',$data);        
	$c = new ContentControllerArticle();       		
	$this->assertEquals(true,$c->save());   		
		
	$article = $c->getSavedArticle();
	$this->assertNotEmpty($article);
	return $article;
}

Our testUpdateArticle will use article created from testAddArticle function, and modify the title of the article from 'test document' to 'test document 2'. Here is the testUpdateArticle function code:

/**
	 * @depends testAddArticle
	 */
public function testUpdateArticle($article){
	$input = JFactory::getApplication()->input;
	$input->set('id',$article->id);    
	$input->set('task','article.save');     
	$data = array(         
		'title'=>'test document 2',//required         
		'catid'=>2,//uncategorised
	);
	$input->post->set('jform',$data);        
	$c = new ContentControllerArticle();       
	$this->assertEquals(true,$c->save());
}

You need to delete 'test document' article created by testAddArticle function before running phpunit. Run phpunit and you should get all tests passed. Now we will create a test function for Trash article process called testTrashArticle, this function will depends on testAddArticle function. Here is the testTrashArticle function:

/**
	 * @depends testAddArticle
	 */
 public function testTrashArticle($article){		
	$input = JFactory::getApplication()->input;			
	$input->set('cid',array($article->id));		   
	$input->set('task','articles.trash'); 		
	$input->set('boxchecked','1');
	//$c = new ContentControllerArticles();       
	//$this->assertEquals(true,$c->publish());
	$controller	= JControllerLegacy::getInstance('Content');
	$ret = $controller->execute(JFactory::getApplication()->input->get('task'));		
	
	$c = new ContentController();		
	$model = $c->getModel('article');	
	$article = $model->getItem($article->id);
	$this->assertEquals($article->state,-2);		
	return $article;
 }

you should delete 'test document 2' article from Article Manager: Articles page before running phpunit again. After you run phpunit you should get all tests passed and the article 'test document 2' status changed to Trashed. Now lets create a test function for delete article process called: testDeleteArticle. This function will depends on testTrashArticle function and here is the code:

/**
	  * @depends testTrashArticle
	  */
	  public function testDeleteArticle($trashedArticle){
		$input = JFactory::getApplication()->input;			
		$input->set('cid',array($trashedArticle->id));		   
		$input->set('task','articles.delete'); 		
		$input->set('boxchecked','1');
		$c = new ContentControllerArticles();       
		$c->delete();
		//$controller	= JControllerLegacy::getInstance('Content');
		//$ret = $controller->execute(JFactory::getApplication()->input->get('task'));		
		
		$c = new ContentController();		
		$model = $c->getModel('article');	
		$article = $model->getItem($trashedArticle->id);		
		$this->assertEquals($article->id,NULL);				
	  }

and as usual you should delete 'test document 2' article from Article Manager: Articles page before running phpunit again. After you run phpunit again you should get all tests passed and you won't find 'test document 2' article on Article Manager: Articles page.

Update June 14, 2014

There are some issues if you run these tests using PHP version > 5.3.x, thanks to valekcrow and angelvperez. You will get this error message if you run phpunit . in command line

1) ArticleTest::testListArticles
unserialize(): Error at offset 210 of 301 bytes
libraries/joomla/input/input.php:341 

I think it is because the PHPUnit modify $_GLOBAL variable for each test function, you can read the description in here http://phpunit.de/manual/3.7/en/fixtures.html#fixtures.global-state. You can fix this problem by moving the following code from bootstrap.php to setUp function in ArticleTest.php.

 $app = JFactory::getApplication('administrator');
//initialize session and user
$session = JFactory::getSession();
$session->set('user', JUser::getInstance('admin'));
//bypass JSession::checkToken()
JFactory::getApplication()->input->post->set(JSession::getFormToken(),'1');

You can see the complete code of bootstrap.php and ArticleTest.php file below

bootstrap.php

<?php
error_reporting(E_ALL);
 
define('_JEXEC', 1);
define('BASEPATH',realpath(dirname(__FILE__).'/../'));
define('JOOMLA_PATH',realpath(dirname(__FILE__).'/../../../../'));
define('JOOMLA_ADMIN_PATH',realpath(dirname(__FILE__).'/../../../'));
$_SERVER['HTTP_HOST'] = 'localhost';
$_SERVER['REQUEST_METHOD'] = 'GET';
 
if (file_exists(JOOMLA_ADMIN_PATH . '/defines.php'))
{
    include_once JOOMLA_ADMIN_PATH . '/defines.php';
}
 
if (!defined('_JDEFINES'))
{
    define('JPATH_BASE', JOOMLA_ADMIN_PATH);   
    require_once JPATH_BASE . '/includes/defines.php';
}
 
require_once JPATH_BASE . '/includes/framework.php';
require_once JPATH_BASE . '/includes/helper.php';
require_once JPATH_BASE . '/includes/toolbar.php';
define('JPATH_COMPONENT',JOOMLA_ADMIN_PATH.'/components/com_content');
define('JPATH_COMPONENT_ADMINISTRATOR',JPATH_COMPONENT);
 
include BASEPATH.'/controller.php';    
include BASEPATH.'/controllers/article.php';
include BASEPATH.'/controllers/articles.php';

 and ArticleTest.php file

<?php
class ArticleTest extends \PHPUnit_Framework_TestCase {
	
	protected function setUp()
    {
        $app = JFactory::getApplication('administrator');
		//initialize session and user
		$session = JFactory::getSession();
		$session->set('user', JUser::getInstance('admin'));
		//bypass JSession::checkToken()
		JFactory::getApplication()->input->post->set(JSession::getFormToken(),'1');
    }	 
	public function testListArticles(){    
        $c = new ContentController();      
        $model = $c->getModel('articles');
        $articles = $model->getItems();
        //var_dump($articles);
        $this->assertNotEmpty($articles);
    }
    public function testSearchArticles(){
        //simulate user request: searching for 'about'
        $input = JFactory::getApplication()->input;
        $input->set('filter_search', 'contact');
         
        $c = new ContentController();      
        $model = $c->getModel('articles');              
        $articles = $model->getItems();
        //var_dump($articles);
        $this->assertNotEmpty($articles);
    }
    public function testAddArticle(){
        $input = JFactory::getApplication()->input;
        $input->set('id',0);   
        $input->set('task','article.save');    
        $data = array(        
            'title'=>'test document',//required        
            'catid'=>2,//uncategorised
            );
        $input->post->set('jform',$data);       
        $c = new ContentControllerArticle();           
        $this->assertEquals(true,$c->save());        
         
        $article = $c->getSavedArticle();
        $this->assertNotEmpty($article);
        return $article;
    }
    /**
     * @depends testAddArticle
     */
    public function testUpdateArticle($article){
        $input = JFactory::getApplication()->input;
        $input->set('id',$article->id);   
        $input->set('task','article.save');    
        $data = array(        
            'title'=>'test document 2',//required        
            'catid'=>2,//uncategorised
        );
        $input->post->set('jform',$data);       
        $c = new ContentControllerArticle();      
        $this->assertEquals(true,$c->save());
    }
    /**
     * @depends testAddArticle
     */
     public function testTrashArticle($article){       
        $input = JFactory::getApplication()->input;         
        $input->set('cid',array($article->id));         
        $input->set('task','articles.trash');       
        $input->set('boxchecked','1');
        //$c = new ContentControllerArticles();      
        //$this->assertEquals(true,$c->publish());
        $controller = JControllerLegacy::getInstance('Content');
        $ret = $controller->execute(JFactory::getApplication()->input->get('task'));      
         
        $c = new ContentController();      
        $model = $c->getModel('article');   
        $article = $model->getItem($article->id);
        $this->assertEquals($article->state,-2);     
        return $article;
     }
      /**
      * @depends testTrashArticle
      */
      public function testDeleteArticle($trashedArticle){
        $input = JFactory::getApplication()->input;         
        $input->set('cid',array($trashedArticle->id));          
        $input->set('task','articles.delete');      
        $input->set('boxchecked','1');
        $c = new ContentControllerArticles();      
        $c->delete();
        //$controller   = JControllerLegacy::getInstance('Content');
        //$ret = $controller->execute(JFactory::getApplication()->input->get('task'));    
         
        $c = new ContentController();      
        $model = $c->getModel('article');   
        $article = $model->getItem($trashedArticle->id);     
        $this->assertEquals($article->id,NULL);              
      }
}

You should run this test using phpunit --stderr . to direct phpunit's output to stderr,

 That's it. I hope from now on we can use TDD in our joomla component development process.