PHPUnit

Mix Prophecy Mock Framework with PHPUnit Mock Builder to Create Mocks for Not Existing Classes and Magic Methods

Submitted by Peter Majmesku on Wed, 08/16/2017 - 12:20

Prophecy is great as long as you are creating mocks for classes or methods, which are existing in your code base. The benefit is, that the usage is a bit more convenient compared with the PHPUnit mock builder (e.g. method definition and parameter handling). Therefor Prophecy is integrated into PHPUnit by default.

If you want to create mocks for classes, which are not existing in your code base or if you want to mock magic methods, this will become a problem. You could create a real class, just to have the class/method. But this is not an nice solution. That's why mixing mocking functionality from the Prophecy mocking framework and the PHPUnit mocking framework makes sense.

Here I am not questioning, if it is good to create tests for classes, that do not belong to your code base. I am just showing a way to handle this.

The following example works with PHPUnit 4.8.35 and up. The Prophecy mocking framework is part of PHPUnit.

// Mock the object via Prophecy - generally more convenient than with PHPUnit itself
$entityManager $this->prophesize(EntityManager::class);

// Create the mock object by PHPUnit, for a class that does not exist.
$shippingMethodRepository $this->getMockBuilder('ShippingMethod')
    ->
disableOriginalClone()
    ->
setMethods(['findByName'])
    ->
getMock();

// Set the not existing method.
$shippingMethodRepository->method('findByName')
    ->
willReturn(['Spedition''Hermes']);

// Define that getRepository() can retrieve any arguments and get a "real" object to test with.
$entityManager->getRepository(Prophecy\Argument::any())
    ->
willReturn($shippingMethodRepository);

// Mock a few methods with Prophecy as you like
$entityManager->persist(Prophecy\Argument::any())->willReturn(null);
$entityManager->flush(Prophecy\Argument::any())->willReturn(null);

// Get the mocked object, which will fake behavior
$entityManager->reveal();

// Will return 'Hermes'
end($entityManager->getRepository('AppBundle:ShippingMethod')->findByName('Delivery')); 

How to use PHPUnit fixtures in your Yii2 development

Submitted by Peter Majmesku on Mon, 08/29/2016 - 19:04

Often you need to create extra database data to run your PHPUnit test, when you're creating your application with Yii2 framework. The best is, that you're doing this work only once and save this with your test, so that you can re-use this test data on every test run. This data is called "fixtures". I want to introduce you my approach of using fixtures with CSV files, which are containing the data which will be written into your database, as you're fireing your PHPUnit test. For programming and running PHPUnit tests I prefer PhpStorm. But you can test your code also with PHPUnit by command line of some configuration in Netbeans or Eclipse with PHP configuration.

PhpMyAdmin and MySQL workbench are allowing you to export CSV files with your database data and the column-names in the first line of the export. The contents of the first line are the names of your properties, which are defined in your Yii model. Ok, let's say you have such exports, you need to have your PHPUnit test.

use app\tests\fixtures\ModelFixtureDataCreator;
class 
MyTest extends PHPUnit_Framework_TestCase
{
    protected function 
setUp()
    {
        require_once 
'yii_booter_for_tests.php';
        
$test_classname get_class($this);
        
ModelFixtureDataCreator::create($test_classname);
    }
    public function 
test()
    {
        
MyController::export(1);
    }
 

At the beginning you see, that I use the ModelFixtureDataCreator(); class for creating my fixtures for the PHPUnit test. The folder structure in my project is the same as in the namespace. I'll explain you the ModelFixtureDataCreator(); class later in detail. The first method in my test is setUp(); which works like a constructor in PHPUnit. I require a file which is booting Yii2 for me, so that I can use it in my unit tests.

if(is_numeric(strpos(getcwd(), '/tests'))){
    require_once 
getcwd() . '/../yii_boot_console.inc.php';
} else {
    require_once 
getcwd() . '/yii_boot_console.inc.php';

This code is very easy, it handles just the paths for me, so that I don't have to mind from which folder I start my tests. This file is just booting Yii's console application. It's just the file you use to run your Gii application from the command line.

I've modified it a bit for my needs (you're free to modify yours as you wish):

<?php defined('YII_DEBUG') or define('YII_DEBUG'true);
// fcgi doesn't have STDIN and STDOUT defined by default
defined('STDIN') or define('STDIN'fopen('php://stdin''r'));
defined('STDOUT') or define('STDOUT'fopen('php://stdout''w'));
require_once(
__DIR__ '/vendor/autoload.php');
require_once(
__DIR__ '/vendor/yiisoft/yii2/Yii.php');
use 
app\controllers\CmPlatformDeterminationClass;
defined('ENVIRONMENT') or define('ENVIRONMENT''dev');
if(!isset(
$argv)){
    
$argv false;
}
// the path to your config file.
$config = require_once('the/path/to/your/config/file.php');
new 
yii\console\Application($config); 

Then I determine the name of my test class and pass it finally to my ModelFixtureDataCreator(); class, where I call the create-method statically:

<?php $test_classname get_class($this);
ModelFixtureDataCreator::create($test_classname); 

The magic from the ModelFixtureDataCreator(); class is, that it uses "convention over configuration". Once you have the following folder structure the class is able to create all fixture data in relation to your test.

- your yii2 application folder
-- tests folder
--- fixtures folder
---- folder exactly named like your test without the file extension (f.e. ExportControllerTest)
----- the csv files exactly named like your models (f.e. MyModel.csv)

 Downwards you can see the code from ModelFixtureDataCreator(); class with it's comments.

<?php amespace app\tests\fixtures;

use 
yii\base\Exception;


/**
 * Class ModelFixtureDataCreator
 * @package app\tests\fixtures
 */
class ModelFixtureDataCreator {

    
/**
     * Reads the CSV files from the test related fixture folder, instantiates
     * the model by the CSV filename without the file-extension and passes
     * it to the writeFixtureDataForModelIntoDb(); method with the modelname
     * and the test classname.
     *
     * @param $test_classname
     */
    
public static function create($test_classname){
        
$path__fixture_folder 'fixtures/' $test_classname;

        
$handle__fixture_folder opendir($path__fixture_folder);

        while ((
$file readdir($handle__fixture_folder)) !== false){
            if(
is_file($path__fixture_folder '/' $file)){

                
$arr__pathinfo pathinfo($file);
                
$str__modelname str_replace('.' $arr__pathinfo['extension'],
                                                                    
''$file);

                
$str__modelpath 'app\\models\\' $str__modelname;

                
$obj__model_instance = new $str__modelpath;
                
self::writeFixtureDataForModelIntoDb($obj__model_instance,
                                            
$str__modelname$test_classname);
            }

        }

    }

    
/**
     * Uses the first line from the CSV file as the model properties. Then
     * the method treats the next lines as values and writes them to the model
     * related database table.
     *
     * @param $obj__model_instance
     * @param $str__modelname
     * @param $test_classname
     */
    
private static function writeFixtureDataForModelIntoDb($obj__model_instance,
                                            
$str__modelname$test_classname){

        
$fp fopen("fixtures/$test_classname/$str__modelname'.csv''r');

        
$first_row_runed false;
        
$properties = array();
        while ((
$data fgetcsv($fp1000",")) !== FALSE) {
            if(
$first_row_runed === false){
                
$properties $data;
            } else {
                
$sub_counter 0;
                foreach(
$data as $d){
                    
$obj__model_instance->$properties[$sub_counter] = $d;
                    
$sub_counter++;
                }
                
$obj__model_instance->save();
            }

            
$first_row_runed true;
        }

    }


The class is currently only writing fixture data into your database. By the tearDown(); method from your PHPUnit test you're able to implement logic, which is deleting the fixture data after your test has been runned.

Do you have any ideas for this approach? Share them!

Subscribe to PHPUnit