Category Archives: yii

#nastyBug Http Codes: Don’t play with me

Wanna tell you story how I spent a lot of time to fix one #nastybug.

I like to begin my story with intrigue ūüôā

Right now I work on mobile app. I do backend side. And we have native iphone app. All was well until one day we started getting errors¬†“request body stream exhausted” OR we just don’t have response from server and got timeout error. Try to¬†google it¬†and you won’t find quick answer what’s wrong. Initially we thought that something wrong with Content-Size. But no…¬†it is clear that nothing is clear

I checked my logs everything is ok. I checked access and return error in json. We checking mobile app and it just doesn’t work.

Finally we started to check all the logs in app in server on mobile side. AAAAAND what we see server return json but with code 100 What the F. Code 100. And here I realized that I have custom error code like 1001, 1002 etc. Ok let’s check the code.

I think everybody how worked with yii and need to return something in JSON write functions like this.

    protected function _returnJson($data, $code = 200)
    {
        $this->layout = false;
        header('Content-type: application/json');
        if ($code) {
            header("HTTP/1.1 {$code} " . $this->getHttpHeader($code));
        }

        echo CJSON::encode($data);
    }

    /**
     * Send json error
     * @param string $error
     * @param null $code
     * @param null $description
     */
    protected function _returnJsonError($error = 'Sorry an error occurred', $code = 400, $description = null)
    {
        $this->layout = false;
        header('Content-type: application/json');
        header("HTTP/1.1 {$code} " . $this->getHttpHeader($code));

        echo CJSON::encode([
            'errors' => [
                'code' => $code,
                'message' => $error,
                'description' => $description
            ]
        ]);
    }

I also wrote such functions at some point. And they “nomadize” from one my project to another. Do you see anything suspicious? Looks good to me.

And these functions have been working really good. Until I need to send specific error code. But STOP it’s too absolute different codes. HTTP code and my custom error codes. And there is no way to mix.

So I’ve reworked interface to

_returnJsonError($error = ‘Sorry an error occurred’, $internalErrorCode = null,
$description = null, $httpCode = 400)

This fixed all the problems. So be careful HTTP codes it’s not for kids ūüôā

Yii migration use change in addition to up and down

Finally I pushed some of my yii extensions to github.
https://github.com/radzserg/yii-Rz

Still need to work on README.md I have lack of time now. All in all I think I can refer to it now.

And as a bonus I will tell about my extension for CDbMigration. A lot of migration tools/lib already support change() method in addition to up() and down(). 

But the main problem here – it’s easy to implement it if we create table or add a column or index in this case we know that we need to do in down action. It’s easy we need to remove appropriate object. For example:

    public function up()
    {
        $this->addColumn('user', 'email', 'varchar(255) AFTER username');
        $this->addColumn('user', 'password', 'varchar(255) AFTER email');
    }

    public function down()
    {
        $this->dropColumn('user', 'email');
        $this->dropColumn('user', 'password');
    }

Easy enough. But what do we need to do in down action if we create table. In this case everything is quite complicated. We either need to keep DB snapshot for each migration. Or create it going through migration until some migration that you need. Both quite difficult to implement and I guess even if you can implement such approach you still have a lot of problem.

The good thing here that in most migrations we add or create something. And we don’t need to care about down function. And in that cases when we need to implement down functionality we will do that using closures.

Here are my code
https://github.com/radzserg/yii-Rz/blob/master/db/ChangeMigration.php

Here are examples of some of my migrations

<?php

class m130626_132012_add_not_confirmed_users extends Rz\db\ChangeMigration
{
    public function change()
    {
        $this->addColumn('sm_user', 'confirmed', 'TINYINT(1) NOT NULL DEFAULT 0 after create_time');
        $this->addColumn('user', 'confirmed', 'TINYINT(1) NOT NULL DEFAULT 0 after create_time');

        $this->alterColumn('sm_user', 'user_id', 'INT', function() {
           $this->alterColumn('sm_user', 'user_id', 'INT NOT NULL');
        });
    }

}

When you run yiic migrate down. Migration will save queue of back actions in this example

  • remove column sm_user.confirmed (autogenerated)
  • remove column user.confirmed¬†(autogenerated)
  • alter sm_user change user_id to int not null (our manual down closure)

And finally execute all this action in reverse order that’s it. No need to care about down functionality and order.

I proposed that approach to yii core team¬†https://github.com/yiisoft/yii2/issues/593#issuecomment-20397820¬†but they refuse it. Well maybe it’s correct when we talk about framework. Anyway I decided to implement and try it. And for now I like it ūüôā

Try it too and let me know do you like it or no.

How do I send emails in my applications

Today I decided to share some of my ideas according to email sending. I will use yii app as an example but it’s absolutely not required. It’s just an aproach that you can use with any other framework. Right now yii doesn’t have default mail component. But I won’t tell you what mail extension or lib should you use. I want to tell you about how should look like email send in your app.

Well I really believe that you already don’t send emails like this ūüôā

$message = " Pellentesque convallis tempor tortor. Nullam nec purus.";
mail('somebody@example.com', 'Nonsensical Latin', $message);

But you do it like this

     $message = \Swift_Message::newInstance()
        ->setSubject('Hello Email')
        ->setFrom('send@example.com')
        ->setTo('recipient@example.com')
        ->setBody(
            $this->renderView(
                'HelloBundle:Hello:email.txt',
                array('name' => $name)
            )
        )
    ;
    $this->get('mailer')->send($message);

It could be Swift or Zend Mailer or other good lib.

What are advantages in this approach

  • First of all you don’t create a wheel but use good and checked tools
  • It doesn’t render email body inside your code. It renders it from some view. Very good, take five

But what are disadvantages in this approach

  • it’s not DRY. In real app you will send dozens of emails. And following this approach you’ll have dozens places with the same piece code.¬†
  • it’s doesn’t scalable at all if tomorrow you want to log sent emails or want to render it in html, or add bcc etc, you have to to add additional logic in dozen of places.¬†
  • it’s quite difficult to update email template. If we have some managers, moderators, admin that sometimes want to improve some text in email they can’t do that without asking developer.

So what is my proposal?

Email send have to look like this

$emailReset = new \MyApp\email\ResetPassword(array(
    'user' => $userModel,
    'hash' => $hash,
    /* other dynamic data for email */
));
$emailReset->send();

And that’s it! You don’t need another code. You create an obejct of required email, in my example it’s reset password email. I pass user model, so then we can get email, username, etc and I pass specific data for that email. And… I send it.

How we can achieve that?
We have various emails in our app Рsign in, reset password, sale emails, promo emails, and many many others. For each email we know subject and body.  The idea is to keep email templates in DB

email

Nothing fancy at that point. Then I add parent class for all my emails

<?php

namespace MyApp\email;

abstract class Base
{

    /**
     *
     * @var TransportInterface
     */
    private static $_transport;

    /**
     *
     * @var \MyApp\model\EmailTemplate
     */
    private $_emailTemplate;
    protected $_emailCode;
    private $_addToEmail;
    private $_addToUsername;
    private $_data;

    /**
     * Recipient
     * @var \MyApp\model\User
     */
    private $_user;

    public function __construct($data)
    {
        $data = $this->_initData($data);

        if (!empty($data['user']) && ($data['user'] instanceof \MyApp\model\User)) {
            $this->_user = $data['user'];
            $data['email'] = $this->_user->email;
            $data['username'] = $this->_user->getUsername();
        } else {
            if (empty($data['email'])) {
                throw new \Exception("Email is not defined");
            }
        }
        $this->_addToEmail = $data['email'];
        if (!empty($data['username'])) {
            $this->_addToUsername = $data['username'];
        }
        $this->_data = $data;
    }

    protected function _initData($data)
    {
        return $data;
    }

    /**
     * Compile and send email
     */
    public function send()
    {
        $email = $this->_emailTemplate();

        $email->compileEmail($this->_data);

        $defaultEmailParams = \Yii::app()->params['email'];
        $mail = new \Zend\Mail\Message();
        $mail->setBody($email->body);
        $mail->setSubject($email->subject);
        $mail->addTo($this->_addToEmail, $this->_addToUsername);
        $mail->setFrom($defaultEmailParams['defaultFromEmail'], $defaultEmailParams['defaultFromName']
        );

        //$mail->addHeader('List-Unsubscribe', '<' . 'link' . '>');
        $this->getTransport()->send($mail);

        return true;
    }

    /**
     * Return email transport
     * @return \Zend\Mail\Transport\TransportInterface
     */
    public static function getTransport()
    {
        if (!self::$_transport) {
            if (\Yii::app()->params['email']['provider'] == 'sendmail') {
                self::$_transport = new \Zend\Mail\Transport\Sendmail();
            } elseif (\Yii::app()->params['email']['provider'] == 'file') {
                $fileOptions = new \Zend\Mail\Transport\FileOptions();
                $fileOptions->setPath(\Yii::app()->basePath . '/runtime/cache/email/' . APPLICATION_ENV);
                self::$_transport = new \Zend\Mail\Transport\File($fileOptions);
            }
        }
        return self::$_transport;
    }

    /**
     * Return email object
     * @return \MyApp\model\EmailTemplate
     * @throws Exception
     */
    protected function _emailTemplate()
    {
        if (!$this->_emailCode) {
            throw new Exception("Template name is not set");
        }
        if (!$this->_emailTemplate) {
            $this->_emailTemplate = \MyApp\model\EmailTemplate::getByCode($this->_emailCode);
        }

        return $this->_emailTemplate;
    }

}

It’s just an example. So your class be different could more functionality that I described above. I show just an approach. I used ZendMail component it’s so easy now if you user composer ( ¬†“zendframework/zend-mail”: “*”¬†) but you wanna use what ever you want. I believe everything is quite straight forward here.

Only the one note about¬†$email->compileEmail($this->_data) I liked to use Twig to render emails. So compileEmail in fact just render twig template that we have in db and¬†¬†copy in file. But it’s another topic. If¬†anyone would be interested I can describe that in more detail.

Finally the example of specific email is following

<?php

namespace MyApp\email;

class ResetPassword extends \MyApp\email\Base
{

    protected $_emailCode = 'reset_password';

    protected function _initData($data)
    {
        $data['link'] = \Yii::app()->createAbsoluteUrl('urltoreset', array(
            'email' => $data['user']['email'],
            'hash' => $data['hash']
        ));

        return $data;
    }

}

That’s it. Hope it would be helpful for someone ūüôā

Yii and ajax error handling

Continue to write about interaction between yii and frontend.

Problem:¬†create common solution to handle ajax errors on frontend. The main problem here is that in case of error yii will return error message as text. If you look inside CErrorHandler you’ll find such code

if($this->isAjaxRequest())
    $app->displayException($exception);
else
    $this->render('exception',$data);

if it’s ajax request we out application class will display exception. In general case it’s CWebApplication that will show you plain text with error details.

But imagine that you make some request on frontend and assuming to get JSON data. If some error happens you get error message. So you need to check somehow is it json and if it’s not json most likely it’s an error message. I’d like to get more smart solution.

Solution:¬†let’s return for AJAX request in all cases result flag something like array(‘result’ => ‘true’, ‘data’ => array(our result data)). And then on frontend we in case of ajax error we will handle it.

So what do we need. Let’s create our own application class.

<?php

namespace Ql\Comp;

class WebApplication extends \CWebApplication
{


    public function displayException($exception)
    {
        $error = '';
        if (YII_DEBUG) {
            $error .= '<h1>' . get_class($exception) . "</h1>\n";
            $error .= '<p>' . $exception->getMessage() . ' (' . $exception->getFile() . ':' . $exception->getLine() . ')</p>';
            $error .= '<pre>' . $exception->getTraceAsString() . '</pre>';
        } else {
            $error .= '<h1>' . get_class($exception) . "</h1>\n";
            $error .= '<p>' . $exception->getMessage() . '</p>';
        }

        if ($this->getRequest()->getIsAjaxRequest()) {
            header('Content-type: application/json');
            echo \CJavaScript::jsonEncode(array(
                'result' => 0,
                'error' => $error,
            ));
        } else {
            echo $error;
        }
    }

}

And now we need to use our application class. Let’s add changes to index.php

require_once(dirname(__FILE__). '/protected/MyNamespace/components/WebApplication.php');
Yii::createApplication('MyNamespace\components\WebApplication', $config)->run();

As you see I don’t use default yii namespace structure. I add my own namespace and then use psr-0 autoload conventions. Nothing fancy but it will work a little bit faster. Oh and you need to add namespace to yii::app().¬†I added this line in config/main.php

Yii::setPathOfAlias(‘MyNamespace’, realpath(dirname(__FILE__) . ‘/../MyNamespace’));

The last trick we do for frontend.

$(document).ajaxError(function(event, jqXHR, ajaxSettings, thrownError) {
    var errorPopupHtml, result;
    result = JSON.parse(jqXHR.responseText);
    if (result.result === 0) {
      errorPopupHtml = JST['app/error']({
        title: "Error",
        error: result.error
      });
      // here I use modal popup from twitter bootstrap but you can use whatever you want.
      $(window.document.body).append(errorPopupHtml);
      return $('#myModal').modal({});
    }
  });

That’s it for today. If someone is interested I can provide instructions for ZF that will do the same job.

Yii and backbone js

I tried Backbone.js. Very nice js library BTW. It’s quite simple but at same time have nice functionality – all you need in order to make your frontend smarter. That’s why I like it. I’d call it jquery style he-he ūüôā – small but powerful.
As I remember Backbone js has grown from some rails project. So it has nice integration with rails.
But I tried to use it with yii. And I need to add some tricks in order to make it work.

So first of all backbone JS sends all info decoded by JSON. You can change data type. But this is way for dweebs (joking :). Unfortunatelly yii has some problems with JSON decoded data and REST. So let’s look what we have

public function getPost($name,$defaultValue=null)
{
return isset($_POST[$name]) ? $_POST[$name] : $defaultValue;
}

In our case POST data will be in “Request Payload” header i.e. we need to use getRawData function to fetch it (or getRestParams() inorder to fetch and parse data)
But look at getRestParams

if(function_exists('mb_parse_str')) {
    mb_parse_str($this->getRawBody(), $result);
} else {
    parse_str($this->getRawBody(), $result);
}

But in our case we have JSON data. I’ve added pull request (btw my first pull request to yii). Shortly we still talking on it. Not sure will it be approved or declined. But you can find it here https://github.com/yiisoft/yii/pull/2059¬†and here¬†¬†Yii forum thread.
In the mean time here’s my custom HttpRequest component. They key is to check $_SERVER[‘CONTENT_TYPE’] and decode before parsing.


<?php

namespace Rz\components;

class HttpRequest extends \CHttpRequest
{

    protected $_restParams;

	/**
	 * Returns request parameters. Typically PUT or DELETE.
	 * @return array the request parameters
	 * @since 1.1.7
	 * @since 1.1.13 method became public
	 */
	public function getRestParams()
	{
		if($this->_restParams===null)
		{
			$result=array();
            if ($this->_isJsonEncoded()) {
                $result = \CJSON::decode($this->getRawBody());
            } else {
                if(function_exists('mb_parse_str')) {
                    mb_parse_str($this->getRawBody(), $result);
                } else {
                    parse_str($this->getRawBody(), $result);
                }
            }

			$this->_restParams=$result;
		}

		return $this->_restParams;
	}


    /**
	 * Returns the named POST parameter value.
	 * If the POST parameter does not exist, the second parameter to this method will be returned.
	 * @param string $name the POST parameter name
	 * @param mixed $defaultValue the default parameter value if the POST parameter does not exist.
	 * @return mixed the POST parameter value
	 * @see getParam
	 * @see getQuery
	 */
	public function getPost($name,$defaultValue=null)
	{
        if ($this->_isJsonEncoded()) {
            $data = \CJSON::decode($this->getRawBody());
            return isset($data[$name]) ? $data[$name] : $defaultValue;
        } else {
            return isset($_POST[$name]) ? $_POST[$name] : $defaultValue;
        }
	}

    /**
	 * Returns the named DELETE parameter value.
	 * If the DELETE parameter does not exist or if the current request is not a DELETE request,
	 * the second parameter to this method will be returned.
	 * If the DELETE request was tunneled through POST via _method parameter, the POST parameter
	 * will be returned instead (available since version 1.1.11).
	 * @param string $name the DELETE parameter name
	 * @param mixed $defaultValue the default parameter value if the DELETE parameter does not exist.
	 * @return mixed the DELETE parameter value
	 * @since 1.1.7
	 */
	public function getDelete($name,$defaultValue=null)
	{
		if($this->getIsDeleteViaPostRequest())
			return $this->getPost($name, $defaultValue);

		if($this->getIsDeleteRequest())
		{
			$this->getRestParams();
			return isset($this->_restParams[$name]) ? $this->_restParams[$name] : $defaultValue;
		}
		else
			return $defaultValue;
	}

	/**
	 * Returns the named PUT parameter value.
	 * If the PUT parameter does not exist or if the current request is not a PUT request,
	 * the second parameter to this method will be returned.
	 * If the PUT request was tunneled through POST via _method parameter, the POST parameter
	 * will be returned instead (available since version 1.1.11).
	 * @param string $name the PUT parameter name
	 * @param mixed $defaultValue the default parameter value if the PUT parameter does not exist.
	 * @return mixed the PUT parameter value
	 * @since 1.1.7
	 */
	public function getPut($name,$defaultValue=null)
	{
		if($this->getIsPutViaPostRequest())
			return $this->getPost($name, $defaultValue);

		if($this->getIsPutRequest())
		{
			$this->getRestParams();
			return isset($this->_restParams[$name]) ? $this->_restParams[$name] : $defaultValue;
		}
		else
			return $defaultValue;
	}

    /**
     * Check is request encoded in JSON
     * @return bool
     */
    protected function _isJsonEncoded()
    {
        if (isset($_SERVER['CONTENT_TYPE']) && strpos($_SERVER['CONTENT_TYPE'], 'application/json') === 0) {
            return true;
        }
        return false;
    }
}

And one more note. If you want to use csrf verification perhaps you need to add such trick to your js

  $(document).ajaxSend(function(event, request, settings) {
    var data;
    if (settings.data) {
      data = jQuery.parseJSON(settings.data);
      data['YII_CSRF_TOKEN'] = $.cookie('YII_CSRF_TOKEN');
      settings.data = JSON.stringify(data);
    } else {
      settings.data = "YII_CSRF_TOKEN=" + encodeURIComponent($.cookie('YII_CSRF_TOKEN'));
    }
  });

Yii with coffee script and less css

Do you already use less css? I do. It’s nice. and css looks really good. And what about coffee script?

There’s great assetic manager¬†that provide such functionality. But how I could use it with yii?¬†¬†I didn’t find good receipt. So I made mine. We extend CClientScript registerScriptFile and¬†registerCssFile functions. We will check .less and coffee extensions. And if so will handle it by exited assetic manager. That’s it. Nothing fancy I did quite fast and didn’t test a lot. It works for me.


<?php

include_once Yii::getPathOfAlias('application.vendors.lessphp') . '/lessc.inc.php';

class RsClientScript extends CClientScript
{

    public function init()
    {
        Yii::setPathOfAlias('Assetic', Yii::getPathOfAlias('application.vendors.assetic.src.Assetic'));
        Yii::setPathOfAlias('Symfony', Yii::getPathOfAlias('application.vendors.Symfony'));

        parent::init();
    }

    public function registerCssFile($url,$media='')
	{
        $url = $this->_handleCssFile($url);
        return parent::registerCssFile($url, $media);
	}

    public function registerScriptFile($url)
    {
        $url = $this->_handleJsFile($url);
        return parent::registerScriptFile($url);
    }

    private function _handleJsFile($url)
    {

        if (preg_match('~^(.)*(.coffee)$~', $url)) {
            $path = Yii::getPathOfAlias('webroot') . $url;

            $filter = new \Assetic\Filter\CoffeeScriptFilter('/usr/local/bin/coffee');
            $asset = new Assetic\Asset\AssetCache(
                new Assetic\Asset\FileAsset($path, array($filter)),
                new Assetic\Cache\FilesystemCache(Yii::getPathOfAlias('application.runtime.assetic_cache'))
            );

            $newUrl = substr($url, 0, -7);
            $newPath = Yii::getPathOfAlias('webroot') . $newUrl;

            file_put_contents($newPath, $asset->dump());

            return $newUrl;
        }
        return $url;
    }

    private function _handleCssFile($url)
    {
        if (preg_match('~^(.)*(.less)$~', $url)) {
            $path = Yii::getPathOfAlias('webroot') . $url;

            $filter = new \Assetic\Filter\LessphpFilter();
            //$filter = new \Assetic\Filter\LessFilter(null, array('/usr/local/lib/node_modules/'));
            $asset = new Assetic\Asset\AssetCache(
                new Assetic\Asset\FileAsset($path, array($filter)),
                new Assetic\Cache\FilesystemCache(Yii::getPathOfAlias('application.runtime.assetic_cache'))
            );

            $newUrl = substr($url, 0, -5);
            $newPath = Yii::getPathOfAlias('webroot') . $newUrl;

            file_put_contents($newPath, $asset->dump());

            return $newUrl;
        }
        return $url;
    }
}

Perhaps in future I will use something more complex. As you see right now even if file wasn’t changed and we have cache for css file you have write them again to result file. The problem that I can’t check does cache exist by¬†Assetic\Asset\AssetCache without extending it.

Well right now I use it only for dev so it’s ok.

Exteneded Yii CConsoleCommand

Continue to write about yii and sharing my experience. Today I write about how do I exteneded CConsoleCommand.First of all I wanna say that base class CConsoleCommand is all what you need to run console commands. And I extended it for my purposes. But everybody can want to do it in another way or add another functionality. So all of this is just an example of extending base class.

I’ve added few additional abilities

  • pretty verbose method (add ./yiic myCommand –verbose)
  • ability to run only one copy of script
  • if verbose is set, show script execution time
<?php
 
/**
 * Extended yii console command
 *
 * @author: radzserg
 * @date: 11.04.11
 */
class ConsoleCommand extends CConsoleCommand
{
 
    const VERBOSE_ERROR = 'error';
    const VERBOSE_INFO = 'info';
    const VERBOSE_SYSTEM = 'system';
 
    public $verbose;
 
    private $_lockFile;
 
    // if false this means that multiple scripts can work simultaneously
 
    protected $_isSingletonScript = false;
 
    // calculate time execution time
 
    protected $_timeStart;
 
    protected function _verbose($message, $level=null, $type=null)
    {
        if (!$this->verbose) {
            return ;
        }
 
        $level = (int)$level;
        $indent = str_repeat("\t", $level);
        if ($type == self::VERBOSE_ERROR) {
            // message in red
            $message = "\033[31;1m" . $message . "\033[0m\n";
        } elseif ($type == self::VERBOSE_INFO) {
            // message in green
            $message = "\033[32;1m" . $message . "\033[0m\n";
        } elseif ($type == self::VERBOSE_SYSTEM) {
            $message = "\033[33;1m" . $message . "\033[0m\n";
        }
 
        echo $indent . date('H:i:s ') . $message . "\n";
    }
 
    protected function beforeAction($action,$params)
    {
        $this->_verbose("Start execution of " . get_class($this), null, self::VERBOSE_SYSTEM);
        $this->_timeStart = $this->_microtimeFloat();
        if ($this->_isSingletonScript) {
            $lockDir = Yii::getPathOfAlias('application.commands.lock');
            if (!is_dir($lockDir)) {
                mkdir($lockDir);
            }
            $filePath = $lockDir . '/' . get_class($this) . '.lock';
            $this->_lockFile = fopen($filePath, "w");
            if (!flock($this->_lockFile, LOCK_EX | LOCK_NB)) {
                $this->_verbose("Another instance of this script is running");
                return false;
            }
        }
        return true;
    }
 
 
 
    protected function afterAction($action,$params)
    {
        if ($this->_lockFile) {
            flock($this->_lockFile, LOCK_UN);
        }
        $time = round($this->_microtimeFloat() - $this->_timeStart, 2);
        $this->_verbose("End (time: {$time} seconds)", null, self::VERBOSE_SYSTEM);
    }
 
    private function _microtimeFloat()
    {
        list($usec, $sec) = explode(" ", microtime());
        return ((float)$usec + (float)$sec);
    }
}

Unit tests for yii modules

Testing yii modules 

I was creating simple yii module (about which perhaps I will write later). Since last time I like TDD more and more I’ve decided to test it. But there is a problem I’d like to move all tests to [my_module]/tests and do not mix them with project tests.

I din’t find any good tutorial on this topic and decided to do it by myself.
Please note: I added only unit tests. But I believe it’s easy to add functional tests and you can do it by yourself.¬†

Ok let’s start first of all we create following test directory structure:

/config
— test.php
/tests
— /fixtures
— /unit
— bootstrap.php
— phpunit.xml

As you see this is the copy of standard yii directory structure except some files. In fixtures and unit folders we will put fixtures and tests files as we do for standard yii tests. phpunit.xml also will leave the same. Of course you can customize it as you need.

<phpunit bootstrap="bootstrap.php" colors="true" converterrorstoexceptions="true" convertnoticestoexceptions="true" convertwarningstoexceptions="true" stoponfailure="false">

    <testsuite name="all tests">
        <directory>./</directory>
    </testsuite>
</phpunit>

In bootstrap.php we will change paths and comment WebTestCase.php including

 $appPath = realpath(dirname(__FILE__) . '/../../../');
 $config = $appPath . '/modules/pct/config/test.php';
 require_once($appPath . '/yii/framework/yiit.php');
 //require_once(dirname(__FILE__).'/WebTestCase.php');
 Yii::createWebApplication($config);

And finally config.php

 $basePath = realpath(dirname(__FILE__) . '/../../..');

 return array(
     'basePath' => $basePath,

      'import' => array(
         'application.modules.[my_module].models.*',
         'application.modules.[my_module].components.*',
        // any other includings
     ),

     'components'=>array(
         'fixture'=>array(
             'class'=>'system.test.CDbFixtureManager',
             'basePath' => realpath(dirname(__FILE__) . '/../tests/fixtures'),
             // redefine basePath for module tests
         ),
         'db'=>array(
             'connectionString' => 'mysql:host=127.0.0.1;dbname=[test_db_name]',
             'username' => '',
             'password' => '',
         ),
     ),
 );

I define the whole config but you can use test config from main application by

return CMap::mergeArray(
 require(dirname(__FILE__).'../../../config/test.php'),
         array()

That’s it. Good testing for you guys and thank you.