Creating a PHP unit test for Sugar 7

In a previous post, we learned how to write JavaScript unit tests using Sugar 7's Jasmine framework.  Today we will learn how to write PHP unit tests using Sugar 7's PHPUnit framework.  If you do not have access to Sugar 7 unit-tests Github repository and you are a current SugarCRM customer or partner then you should request access.

This post will assume that you are already familiar with PHPUnit fundamentals.  If not, you can check out their Getting Started Guide.

Even if you do not have access or choose not to use the Sugar 7 PHPUnit framework, readers should still find the concepts covered in this post useful for testing Sugar 7 code.

Testing a Logic Hook

We will start with a realistic code customization so we can create a useful example of some unit tests.  There are a variety of server-side code customizations that are possible within Sugar 7 but perhaps the most common example would be the Logic Hook.  If you've done any significant amount of development on Sugar then you've like likely written more than one logic hook.  Sugar logic hooks have been an important tool in the Sugar Developer's toolbox since well before the release of Sugar 7.  So this is a very appropriate example for us to use.

Below you will see an example of a before_save logic hook for the Accounts module.  If you read the code, the use case is quite simple.  When an Account record is saved we check the current account type and if it is an Analyst account then we set the Account's industry to be Banking.  This is accomplished with only a handful of lines of code.

You can install this logic hook locally by copying AccountsOnSaveHooks.php to the custom/modules/Accounts directory and copying setIndustryOnSave.ext.php to custom/Extension/modules/Accounts/Ext/LogicHooks directory.

AccountsOnSaveHooks.php

<?php

/*
* Copyright 2015 SugarCRM Inc.
*/

/**
* Example of a module logic hook that we will unit test
*/
if (!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');

class AccountsOnSaveHooks
{
function setIndustryForAnalystAccount($bean, $event, $arguments)
    {
//generic condition
if ($bean->account_type == 'Analyst')
        {
//update
$bean->industry = 'Banking';
        }
    }
}

setIndustryOnSave.ext.php
<?php
/*
* Copyright 2015 SugarCRM Inc.
*/
$hook_array['before_save'][] = Array(
//Processing index. For sorting the array.
1,

//Label. A string value to identify the hook.
'before_save example',

//The PHP file where your class is located.
'custom/modules/Accounts/AccountsOnSaveHooks.php',

//The class the method is in.
'AccountsOnSaveHooks',

//The method to call.
'setIndustryForAnalystAccount'
);

Creating our first PHPUnit test

If you follow the steps to deploy the Sugar 7 unit tests (for example, here are the steps for installing unit tests in Sugar 7.6), you will have a test/ directory that is added to your Sugar 7 installation that includes many test files.  You will also have PHPUnit installed under the vendor/ directory.



By convention, the tests/ directory mirrors the file structure of the rest of the Sugar application.

This means that when creating tests for our AccountsOnSaveHooks class, we will be creating them within a new file under the tests/custom/ directory.

To try our Sugar 7 PHPUnit test example, copy AccountsOnSaveHooksTest.php below to tests/custom/modules/Accounts/ directory.  This file contains a new AccountsOnSaveHooksTest class where we have implemented several unit tests.

AccountsOnSaveHooksTest.php

<?php
/*
* Copyright 2015 SugarCRM Inc.
*/

require_once 'custom/modules/Accounts/AccountsOnSaveHooks.php';

/**
*  Example tests for our custom Logic Hook.
*/
class AccountsOnSaveHooksTest extends Sugar_PHPUnit_Framework_TestCase
{

private $bean; //Accounts bean
private $hooks; //Hooks class

/**
     * Set up before each test
*/
public function setUp(){
parent::setUp();
$this->bean = BeanFactory::newBean('Accounts');
$this->hooks = new AccountsOnSaveHooks();
/**
         * Use SugarTestHelper to set up only those Sugar global values that are needed.
         * Framework will tear these down automatically after each test.
*/
SugarTestHelper::setUp("beanList");
    }

/**
     * Example that relies on SugarTestHelper
*/
public function testBeanListLoaded(){

global $beanList;
$this->assertNotEmpty($beanList["Accounts"], "This test relies on Accounts bean.");
    }

/**
     * Verify that logic hook changes Industry value when necessary
*/
public function testSetIndustryForAnalystAccount_ChangingIndustry()
    {
$bean = $this->bean;
$bean->account_type = "Analyst";
$this->assertNotEquals($bean->industry, "Banking");
//Mock event
$this->hooks->setIndustryForAnalystAccount($bean, "before_save", array());
//Verify that Banking industry is now set
$this->assertEquals($bean->industry, "Banking");
$this->assertEquals($bean->account_type, "Analyst");

$bean->industry = "Apparel";

$this->hooks->setIndustryForAnalystAccount($bean, "before_save", array());
//Verify that Banking industry is reset
$this->assertEquals($bean->industry, "Banking");
$this->assertEquals($bean->account_type, "Analyst");
    }


/**
     * Verify that Industry value is ignored by logic hook for other changes
*/
public function testSetIndustryForAnalystAccount_IgnoreIndustry()
    {
$bean = $this->bean;
$bean->account_type = "Reseller";
$this->assertNotEquals($bean->industry, "Banking");
//Mock event
$this->hooks->setIndustryForAnalystAccount($bean, "before_save", array());
//Verify that Banking industry is ignored
$this->assertNotEquals($bean->industry, "Banking");
$this->assertEquals($bean->account_type, "Reseller");

$bean->industry = "Apparel";

//Mock event
$this->hooks->setIndustryForAnalystAccount($bean, "before_save", array());
//Verify that industry is stays as Apparel
$this->assertEquals($bean->industry, "Apparel");
$this->assertEquals($bean->account_type, "Reseller");

    }


/**
     * Test that our logic hook can handle unexpected values
*/
public function testSetIndustryForAnalystAccount_UnexpectedValues()
    {
$bean = $this->bean;

//Expected values are not set on bean
unset($bean->industry);
unset($bean->account_type);

$this->hooks->setIndustryForAnalystAccount($bean, "before_save", array());
$this->assertEmpty($bean->industry);
$this->assertEmpty($bean->account_type);


//Unexpected data type
$bean->account_type = -1;

$this->hooks->setIndustryForAnalystAccount($bean, "before_save", array());
$this->assertEmpty($bean->industry);
$this->assertEquals($bean->account_type, -1);

    }

}

You can run these tests by using the phpunit executable installed under the vendor/ directory.  You do need to make sure that tests/ is your current working directory when you run Sugar 7 PHPUnit tests otherwise it will not work

$ cd tests/

$ chmod +x ../vendor/bin/phpunit

$ ../vendor/bin/phpunit custom/modules/Accounts/AccountsOnSaveHooksTest.php

Your output from this command should look similar to this example below.

PHPUnit 4.1.4 by Sebastian Bergmann.

Configuration read from /Users/mmarum/Sites/SugarEnt-Full-7.6.0.0/tests/phpunit.xml.dist

....

Time: 341 ms, Memory: 53.25Mb

OK (4 tests, 15 assertions)

Some Sugar 7 PHPUnit framework features

There are a couple of features that we should call to your attention in the above example.

Sugar_PHPUnit_Framework_TestCase class

All of your Sugar 7 PHPUnit tests should extend the Sugar_PHPUnit_Framework_TestCase class.  This class provides a wrapper that ensures your test behaves consistently as well as enables the use of SugarTestHelper functions.

SugarTestHelper class

SugarTestHelper is the utility that you should use to mock out your necessary test dependencies.  For example, if you need to ensure that the $current_user is set to a valid value then you can use SugarTestHelper::setUp("current_user") to create a dummy user for the purposes of your test and ensure that it is removed after your test is complete.  Inspecting tests/SugarTestHelper.php will reveal all the different utilities available to you as you write your tests.

Advice for creating good Sugar 7 PHPUnit tests

For this example test, we could have tried to set it up so that we call $bean->save() in order to trigger the before_save logic hook indirectly.  This can seem like a reasonable approach because normally we would never call a logic hook function directly.  However, calling the save() function on a bean has other side effects.  For example, we could end up trigger multiple logic hooks when we are only interested in testing one for any given unit test.  We could also end up writing changes into the database that we would need to be cleaned up afterward.  Such DB writes will slow down the speed of your test execution considerably as your test suite grows.

Avoid causing DB transactions in your unit tests!
  • Hi ,

    I tried the same method in Sugar 8.0.1 version with PHPUnit 7.5.0.But when I tried to run the following.

    ..\..\vendor\phpunit\vendor\bin\phpunit custom\modules\Accounts\AccountsOnSaveHooksTest.php

    I got the following error in php error log

    PHP Fatal error:  Uncaught PHPUnit\Runner\Exception: Class 'AccountsOnSaveHooksTest' could not be found in 'custom\modules\Accounts\AccountsOnSaveHooksTest.php'.

    What is the reason for this error and How can I solve this.