Towards A More Testable Module
What can you do to start automated unit testing when you're writing extensions for a complex existing system like Joomla? It's not always easy, but with a few simple changes you can get started. Once you've started, remember the first rule, "Break all Dependencies!" Using some special capabilities built into PHPUnit, along with some of these simple techniques and patterns, you can get to the land of testability.
(While this covers many testing basics, this is not a "Testing 101" article. It assumes not only a working knowledge of PHP, but of some concepts of object oriented programming, automated unit testing, and some familiarity with PHPUnit. It further assumes you either have PHPUnit installed on your current executable path or configured in your IDE. I use the command-line to run the tests in this article. Configuring your particular IDE to properly run the tests is left to you.)
Automated unit testing is one of the most useful tools in a developer's toolbox. It helps focus your mind on the task at hand, it shows you when you've unintentionally broken something that used to work, narrows the range of code where a bug may be hiding, even lets you know when you're done coding.
But how do we bring a tool like that into Joomla? Unit testing assumes you are testing the unit of code in complete isolation from the rest of the system, so that every flaw that gets exposed by testing belongs only to that unit of code.
Traditionally in Joomla, we've tested modules by installing them, clicking some links and watching what happens. Or, for the really brave, letting Selenium click the links. This brings thousands of extra lines of code into the testing process, each one bringing its own potential for defects. It also makes the testing take longer than it should, as those thousands of lines have to execute just so you can see the result of the execution of the ten or fifteen lines of Code That Really Matters.
I'd like to suggest a new approach to creating a module which leverages everything automated unit tests bring, and which can fully test a module in seconds.
I'm using the Random Image module for this example. I selected it partly because it's a simple module so the techniques can be more clearly demonstrated, and partly because I was wanting to add a couple of features to it so I was going to be making it testable, anyway.
Currently, mod_random_image starts out similarly to most modules, with a clump of procedural code in the module entry point file:
// no direct access defined('_JEXEC') or die; // Include the syndicate functions only once require_once dirname(__FILE__).'/helper.php'; $link = $params->get('link'); $folder = modRandomImageHelper::getFolder($params); $images = modRandomImageHelper::getImages($params, $folder); if (!count($images)) { echo JText::_('MOD_RANDOM_IMAGE_NO_IMAGES'); return; } $image = modRandomImageHelper::getRandomImage($params, $images); $moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx')); require JModuleHelper::getLayoutPath('mod_random_image', $params->get('layout', 'default'));
This is clearly not testable. Something has to be done.
When we're confronted with code as aggressively untestable as this, we have two choices: we can live with it as it is, or we can make as few changes to it as necessary to make it testable. Since we don't need to make sweeping changes to this code to make it testable, we'll opt for the latter.
The Object Of The Game
Unit Testing is built around the concept of objects. And, in fact, the module itself is built as an object, with the class defined in the "helper.php" file. (It seems a bit bizarre and anti-semantic that the class is named as if it were a "helper" and not a module, but we're committed to making only as few changes as absolutely necessary, so we'll leave it that way.)
Since the module is defined as an object, why not take the next logical step and treat it like an object, instead of like a namespaced function library? That would mean we could move all the procedural code in the entry point file into methods of the module object. All we'll need to do in the module entry point file is create the object and ask it for its output.
What do we need to have to create the object? The $params registry stores the module options, so the created object will need to know about it. And that appears to be all this module needs to know up front, so we add the params registry as a class attribute, and create the class constructor:
/** * @var params the params registry for the module */ protected $params; /** * * Constructor. * * @param JRegistry $params * * @return modRandomImageHelper */ public function __construct( $params ) { $this->params = $params; }
It's a simple constructor; all it does is store the params registry locally so the class methods can access it. The params registry has all the options the module will need, and they can be pulled out later when used. At this point, if we had wanted to, we could have created a class attribute for every individual piece of data in the params registry (width, height, folder, etc.) and stored them instead. But we're trying to make as few changes as possible in existing code, to minimize the possible side effects. Getting this module under test is the current goal; refactoring it will come later.
The majority of the procedural code in the module entry point file has to do with producing the output of the module. Since we now want the object to do that, we'll need to move that code into a class method to produce the output:
/** * * createOutput. * * This method outputs through the selected template the results of the * module. * * @return none * */ public function createOutput() { $link = $this->params->get('link'); $moduleclass_sfx = htmlspecialchars($this->params->get('moduleclass_sfx')); $folder = static::getFolder($params); $images = static::getImages($params, $folder); if (!count($images)) { echo JText::_('MOD_RANDOM_IMAGE_NO_IMAGES'); } else { $image = $this->getRandomImage($images); require JModuleHelper::getLayoutPath('mod_random_image', $params->get('layout', 'default')); } }
If you look at this code, you'll see it's pretty much the same code as was in the original entry point file. The modRandomImageHelper:: references have been changed to be simply static:: but that's about it. They work the same way well, pretty much the same way) but the reason for the change will appear later; for now, we'll just do it.
I should note that I could have rendered the JModuleHelper static call harmless by making this class a child of JModuleHelper and replacing it with "$this->getLayoutPath" (indeed, I did that in an earlier draft of this approach) but that meant I would have to introduce enough of Joomla to define JModuleHelper properly, and then deal with the avalanche of hard-coded static dependencies in the inherited method. When trying to bring a legacy system under test, it's best to take small bites. But that's certainly a refactoring to keep in mind down the road, after more dependency clearing has taken place.
// no direct access defined('_JEXEC') or die; // Include the syndicate functions only once require_once dirname(__FILE__).'/helper.php'; $randomImage = new modRandomImageHelper($params); $randomImage->createOutput();
You can see it still contains procedural code; that can't be helped because that's what Joomla is demanding we have there. (Perhaps a future version of Joomla could simply look up the name of the object and invoke it, but that affects more than just this module, so it's a decision to be made later.) But this procedural code is more easily tested (the entry point code will be in our final test file, in fact). And, sure enough, we find when we make these changes to the Random Image module code in an existing Joomla installation, it still works. That's as confident as we can get without having automated testing that we haven't broken things, so now we can start setting up the automated tests.
These Are The Lines That Test Our Souls
There are probably as many different ways to set up a testing environment as there are developers. I'll show you the way I'm used to doing it, feel free to take what you like and modify the rest. The important thing is not that you test my way, but that you test, period. And you're more likely to test if you're comfortable with your testing setup.
First, I create a "test" directory to hold all the files I'll be using in my tests. Beneath that I create a "suite" directory, which will hold the automated test files, and a "fixtures" directory to hold all of the support files for my tests.
PHPUnit uses a "bootstrap" file to create the environment for the tests to run, so directly under the "test" directory I create a file called "bootstrap.php" which contains:
<?php define('_JEXEC', 1); define('JPATH_BASE', __DIR__ . '/fixtures'); define('JPATH_SITE', __DIR__ . '/fixtures');
The only one we need for sure right now is _JEXEC, but we'll need the others by the time we're done, so go ahead and stick them all in now, while we're at it.
We're going to put our automated test file in the "suite" directory. We'll call it "helperTest.php" (PHPUnit wants all automated test files to end with "Test", and besides, making your test file names be the name of the file under test plus "Test" is a good habit to get into). It will contain:
include_once __DIR__ . '/../../helper.php'; /** * modRandomImageHelperTest * * Proposed phpunit testing approach for the module Random Image */ class modRandomImageHelperTest extends PHPUnit_Framework_TestCase { /** * @var module the module under test */ protected $module; /** * setUp * * Creates the module under test and sets everything else up for the test. */ protected function setUp() { $this->module = new modRandomImageHelper(); } /** * testCreatedModule * * Tests the just-created module to ensure it's set up properly. */ public function testCreatedModule() { } }
This the basic test skeleton we'll be building on during the rest of this article. We could try running it at this point, but it would die with an error, because we're not sending in a params registry.
We could create the official Joomla params registry for it by bringing in the JRegistry object, but again, that would bring in not only JRegistry, but all of its dependencies as well, and the first rule of proper unit testing is "Break All Dependencies." So what should we do?
Mocking The Turtle
Looking over the code, the only method we ever call on the params object is get, and that's a simple enough method to duplicate. We're going to create a "test double," an object we'll use in the test that will pretend it's the params registry, and the code under test won't be able to tell it isn't a real registry.
One way we can do this is to write our own test-specific object that only does what the test needs it to do. So in the fixtures directory (remember, it's for our test support files) we'll create a file called "mockParams.php" and it will contain the minimum amount of code necessary (remember, less code === fewer bugs) to carry off the deception:
<?php class mockParams { public $params; public function get($param, $default=null) { return isset($this->params[$param]) ? $this->params[$param] : $default; } }
Simple and straightforward. Since the only JRegistry method our module uses is get(), the code under test will think it's talking to the "real" params registry.
This means we need to include the class definition in the test file, and initialize our fake params in the test setup:
include_once __DIR__ . '/../../helper.php'; include_once __DIR__ . '/../fixtures/mockParams.php';
and
protected function setUp() { $this->params = new mockParams(); $this->params->params['width'] = null; $this->params->params['height'] = null; $this->params->params['link'] = '/'; $this->params->params['folder'] = 'images'; $this->params->params['type'] = 'jpg'; $this->params->params['moduleclass_sfx'] = null; $this->params->params['layout'] = null; $this->module = new modRandomImageHelper($this->params);
Now we need to prove the new module was created properly. Normally, we could do this with a simple test assertion. something like:
$this->assertEquals($this->module->params, $this->params)
This line tells PHPUnit that we think the module params attribute is the same object as the params attribute we created for our test class. It would be if the code in the constructor executed properly, and won't be if it didn't. But there's a problem with that approach. In the class definition file there is:
/** * @var params the params array for the module */ protected $params;
The params registry was specified as "protected." This means it can't be accessed outside of the module, so the assertion would fail with an error, even if the code ran properly.
We have two choices again. One, we could go back and change the "protected" to "public," which would make it visible to all code, including the test. But that also makes it changeable by any code that can find it, making it a possible future target for an unpleasant side effect. Still, in this case, I think the risk is minimal and we could do that safely.
But what if the risk wasn't minimal? What if you really needed that attribute to be protected? Have we already broken the testability simply by requiring that?
No, because PHPUnit contains some special methods that can be used to test private and protected attributes:
public function testCreatedModule() { $this->assertAttributeEquals($this->params, 'params', $this->module); }
The test assertion “assertAttributeEquals” can pull a private or protected attribute's value out (it can't change it, only read it) and compare it with your expected value. In this specific case, we're telling it to compare $this->params with the attribute 'params' from the object $this->module, and if they're equal, it passes the test. And it does:
$ phpunit --bootstrap bootstrap.php suite PHPUnit 3.6.10 by Sebastian Bergmann. . Time: 0 seconds, Memory: 5.75Mb OK (1 test, 1 assertion)
This is run from the command line (note the addition of the bootstrap parameter, to tell PHPUnit where to find the bootstrap file). I generally run PHPUnit from the command line. Since I run it multiple times during any given development session, I keep it in its own terminal window and simply up-arrow to repeat the command. My system saves the contents of the terminal window from one session to the next, so I never have to type the command more than once.
If you aren't so lucky, instead of retyping the bootstrap parameter you can create a configuration file to tell PHPUnit where to find the bootstrap file. PHPUnit looks in the current directory for this configuration file, so it belongs in the directory you're running PHPUnit from.
The response we received from running it may need some explaining, if you haven't run PHPUnit from the command line before. First, it identifies which version of PHPUnit it's running. (Yes, I'm behind, the current version is up past 3.7. I'll update soon, I promise.)
Then it follows with a line containing a single "." which indicates how many tests are being run (just one in this case) and what happened. The "." means a passed test. If we change what we're testing, for example change assertAttributeEquals to assertAttributeNotEquals, the test fails and the response would look like this:
$ phpunit --bootstrap bootstrap.php suite PHPUnit 3.6.10 by Sebastian Bergmann. F Time: 0 seconds, Memory: 5.75Mb There was 1 failure: 1) modRandomImageHelperTest::testCreatedModule Failed asserting that mockParams Object ( 'params' => Array ( 'width' => null 'height' => null 'link' => '/' 'folder' => 'images' 'type' => 'jpg' 'moduleclass_sfx' => null 'layout' => null ) ) is not equal to mockParams Object ( 'params' => Array ( 'width' => null 'height' => null 'link' => '/' 'folder' => 'images' 'type' => 'jpg' 'moduleclass_sfx' => null 'layout' => null ) ). FAILURES! Tests: 1, Assertions: 1, Failures: 1.
This is accompanied by the line number in the test file where the failure happened, leading you directly to the problem assertion.
Are We There Yet?
So, we've changed the module so it appears to be more testable, and managed to create it without a problem. We've got everything nailed, right?
Alas, no. We're just getting started. Looking at the code, the next method we're going to test will be getRandomImage. We're choosing it, because looking at it we can see there are no dependencies on other code that we need to break, so it will be the easiest one to take next (and I'm all about easy, especially while getting used to some new software).
So, what do we need to change? Right off we see it's designated as a static method. Static methods are death to testability (just think about it -- you break dependencies by substituting some test code for an external call, you can't do that with static calls) so we'll eliminate that right now, changing it to a public method. The first two lines reference the params registry as an argument. Since it's a class attribute, we don't need it as an argument, so now the method begins:
public function getRandomImage($images) { $width = $this->params->get('width'); $height = $this->params->get('height');
And that looks like all the changes we need to make to this. So let's write the test for it:
/** * testGetRandomImage * */ public function testGetRandomImage() { $this->params->params['width'] = null; $this->params->params['height'] = null; $expectedWidths = array(100,100); $expectedHeights = array(61,73); $myImages = array( (object)array("name" => 'EQ.jpg', "folder" => 'images', 'width' => 416, 'height' => 304), (object)array("name" => 'hobok.jpg', "folder" => 'images', 'width' => 450, 'height' => 277), ); $image = $this->module->getRandomImage($myImages); $this->assertContains( $image, $myImages); /* tests for object in array sent */ $this->assertContains( (int)$image->width, $expectedWidths, "Incorrect Width"); $this->assertContains( (int)$image->height, $expectedHeights, "Incorrect Height"); }
If that seems a bit much, take a deep breath and relax. Let's walk through it:
The first thing we do is set some module params. Since this is the same object as in the module under test (the module under test has $this->params in it, remember?) setting $this->params sets them in the object as well (a side effect of the way I built the object, and one which we're really going to exploit in just a little while). We also set some arrays for the expected widths and heights for the two images. We need them to be arrays, because we don't know which of the two images the system will return (it's random, remember) so we prepare for both.
What two images? Glad am I that question you have asked. Remember the fixtures directory? It's time to add some more support files. In this case, I created a directory just below fixtures called "images" and inside it placed two images (their names and sizes are shown above in the $myImages array).
The $myImages array contains the data that the getImages method would return (name and folder) plus two other data points (actual height and width) I wanted to have ready to hand just to help me do the math for the test. If you don't want there reminder, you can drop them.
Since we're sending in module parameters of null for height and width, getRandomImage will assume a width of 100 (read the code) and calculate a height to match the original proportions.
We use assertContains for the test assertion here, because that asserts the actual value is contained as an element of the variable given. In this case, we're asserting the image returned is one of the images contained in the original array given to it, and that the height and width of the returned image are contained in the array of possible correct results.
But this only tests for both values being null. There are three possible conditions for each value (null, less than actual, not less than actual -- the code never tests for equal to actual). Do we have to copy and paste this code nine times to test all nine combinations of conditions?
No, thankfully. PHPUnit provides a way for running the same test multiple times with different sets of test data. First, we need to create a method that returns an array of the data we will be changing. In this case, that's the two module parameters (height and width). and the two arrays of expected widths and heights that could be returned:
/** * casesRandomImage * * Provides test cases for getRandomImage */ public function casesRandomImage() { return array( array( null, null, array(100,100), array(61,73) ), array( null, 200, array(100,100), array(61,73) ), array( null, 400, array(100,100), array(61,73) ), array( 400, null, array(400,400), array(246,292) ), array( 400, 200, array(273,324), array(200,200) ), array( 400, 400, array(400,400), array(246,292) ), array( 800, null, array(416,450), array(304,277) ), array( 800, 200, array(273,324), array(200,200) ), array( 800, 400, array(416,450), array(304,277) ), );
There we have it, all nine combinations covered. But how do we connect the test cases to the test?
* @dataProvider casesRandomImage */ public function testGetRandomImage($testWidth, $testHeight, $expectedWidths, $expectedHeights) { $this->params->params['width'] = $testWidth; $this->params->params['height'] = $testHeight; $myImages = array( (object)array("name" => 'EQ.jpg', "folder" => 'images', 'width' => 416, 'height' => 304), (object)array("name" => 'hobok.jpg', "folder" => 'images', 'width' => 450, 'height' => 277), ); $image = $this->module->getRandomImage($myImages);
First we add the special comment line that tells PHPUnit to include the data from @dataProvider. Then we add an argument to the test itself for each element in a row of test data. Since we have nine rows, this test will be run nine times, once with each row of the array from casesRandomImage.
$ phpunit --bootstrap bootstrap.php suite PHPUnit 3.6.10 by Sebastian Bergmann. .......... Time: 0 seconds, Memory: 5.75Mb OK (10 tests, 28 assertions)
And we see 10 dots (one for each of the nine rows of test cases, plus the original test) and the summary of 10 tests run with 28 assertions. (There were three assertions in each of the nine runs, for 27, plus the original assertion.)
And that's also the end of the easy tests.
If The Road Rises To Meet You, You're Walking Uphill
The next method to test should be getFolder(), because it's what tells getImages where to look for the images. But as we look inside it we see static calls into JURI and JString, so we can't execute it unless we want to start bringing chunks of Joomla into our test. If we do that, and a test fails, we won't know if it fails in our code, or in Joomla's, which rather defeats the purpose of unit testing in the first place. Remember, "Break All Dependencies."
So we turn to getImages(), trying to find a way to break its dependency on getFolder(). One way, of course, is for it just to accept the folder value from the params registry and go. But that's not the way we want this code to function "in the wild." Another way is to simply insist that anyone who calls getImages() must have called getFolder() first, passing the resulting value into it as an argument.
The second approach is acceptable, and I might have chosen to do it that way, but for this article I wanted to show another way to deal with this problem that PHPUnit makes available. By throwing a second static method into the mix, we can take advantage of the late static binding properties of PHP 5.3 to make some static method calls replaceable for testing purposes without making code changes.
First, we make these changes to getImage():
static function getImages($theFolder, $type) { $folder = static::getFolder($theFolder); $files = array(); $images = array(); $dir = JPATH_BASE . '/' . $folder;
Yes, we just declared it a static method. Yes, I said static methods are death to testability. And Digitalis is also a poison, but when you have a specific illness, a small dose of it can save your life.
Because the method is now static, it doesn't have access to the local params registry, so we'll pass the values as arguments to it. We'll call getFolder with the static:: keyword we used earlier to enable us to take advantage of PHP's late static binding, and store the result in a local variable. We don't need to get the type from the params registry inside getImages() because we're now passing it in.
How does that help us? Let's look at the new testGetImages() in our test file:
/** * testGetImages */ public function testGetImages() { $mockMe = $this->getMockClass('modRandomImageHelper', array('getFolder')); $mockMe::staticExpects($this->any()) ->method('getFolder') ->with($this->equalTo($this->folder)) ->will($this->returnValue($this->folder)); $images = $mockMe::getImages($this->folder, $this->params->params['type'] = 'jpg'); $this->assertEquals($images, array( (object)array("name" => 'EQ.jpg', "folder" => 'images'), (object)array("name" => 'hobok.jpg', "folder" => 'images'), ));
Huh? What's this "mock" stuff?
Mock objects are test doubles temporarily created for the purposes of the test, like the mockParams() class we created earlier.
In this case, we're telling PHPUnit to create a class that we can use to mock modRandomImageHelper, our class under test, and we're telling it we want it to mock the method getFolder() (mocking a method is to replace the method, either with one of its own or with an empty method).
We then call mockMe, our "fake" class, telling it to expect a static call ($this->any() tells it that any number of these calls are allowed -- we also could specify an exact number that should be allowed) to the mocked method getFolder with an argument equal to the folder attribute of the class method, and that it should return that same value.
The next call triggers the late static binding I spoke of and makes all this possible. We use the mock class to make the static call to the "real" method getImages(). Since we told PHPUnit only to mock the getFolder() method, this call is forwarded to the "real" getImages() method, the code we want to test.
And this is where the magic happens (it all hangs on the slight difference in the way PHP handles "static::" instead of "className::"). Since we called getImages() statically from the mockMe class, the "static::getFolder()" call will reference mockMe, not the class it's written in (had we used "modRandomImageHelper::" it would have bypassed the mock and gone right to the "real" class instead of the mock). when we make that call under "normal" circumstances from inside the "real" object, it would reference modRandomImageHelper.
Therefore, we have managed to call our own test routine in place of getFolder(), which returns the test class attribute "folder."
Now we add the folder class attribute:
/** * @var folder the test folder name */ protected $folder = 'images';
at the top of the test class definition and initialize it with:
$this->params->params['folder'] = $this->folder;
Replacing the similar line in the params initialization in the setUp() method.
Now when we run the test code, the call to getImages will return with an array of the images in our fixtures/images directory, and the assertion will pass:
$ phpunit --bootstrap bootstrap.php suite PHPUnit 3.6.10 by Sebastian Bergmann. ........... Time: 0 seconds, Memory: 6.50Mb OK (11 tests, 30 assertions)
Wait! There was only one assertion in that test. Why did the number of assertions go up by two? Remember this line?
$mockMe::staticExpects($this->any()) ->method('getFolder') ->with($this->equalTo($this->folder)) ->will($this->returnValue($this->folder));
This is an assertion as well. In this specific case it's pretty much the same as "assertTrue(true)" because it will pass, no matter how many times it gets called. To see what happens when this assertion fails, we'll change "$this->any()" to "$this->never()":
$ phpunit --bootstrap bootstrap.php suite PHPUnit 3.6.10 by Sebastian Bergmann. ..........F Time: 0 seconds, Memory: 6.75Mb There was 1 failure: 1) modRandomImageHelperTest::testGetImages modRandomImageHelper::getFolder('images') was not expected to be called. FAILURES! Tests: 11, Assertions: 28, Failures: 1.
While developing tests involving mocked objects, I typically use any() at first, to mock the object while keeping this assertion from triggering and stopping the test, until the behavior of the rest of the test is verified. Then I change it to the correct value. But that's just a habit I got into while I was still learning how mocks worked, feel free to put the correct value in right at the start. In this case, that value should be $this->once() as the mocked method should only get called once.
The technique we used here to mock out static method calls unfortunately only works for static calls within the same class, so this technique cannot be used to mock out the static calls to JURI and the rest.
Note how quickly the tests have been running up to this point -- under one second. Now we're stymied; we'll have to slow down both the testing and the bug fixing now by including Joomla in our test.
Or will we?
Hide Globally, Call Locally
It's true. We could stop here. We've brought nearly 2/3 of the module under test. We could leave the rest for integration testing (testing inside the complete Joomla installation) if we like and we'd still have a net gain. We could create new test cases for reported bugs, and use these tests to see if the problem is in our code, or our understanding of Joomla.
Or we could press on, and test all of our code.
Whenever a method uses globals it becomes difficult to test, and PHP static method calls are merely an insidiously seductive version of globals. But there's a standard pattern for dealing with global references in testing legacy code: We make them not-global. It's called Encapsulate Global References, and we can follow it by gathering the global references into their own object and injecting it into the object under test. That way the object calls to them indirectly, and we can inject a test object to "intercept" the calls to the static methods.
Doing this is not an unalloyed benefit. While it gives us a seam that we can use to test our calling code, it also adds another layer to those calls, which means a small, though real, performance hit. (For most modules, I doubt it would add more than a few milliseconds to the total execution time.) Like everything else, this involves a cost/benefit judgement, because we do receive benefits in exchange for taking the hit.
If we write more than one module, we can share the globals object among them all. When Joomla changes, all we need do is check our globals object code to ensure it still calls into Joomla correctly and receives the correct values back, and we know all our modules will still be working. And, if Joomla has changed to the point it doesn't work, all we need do is make our changes once, in our globals object code, and all of our modules will now work.
Also, by using it as an interface layer to Joomla, we may find we can reuse our module code in other projects, just by replacing the Joomla-specific globals object with one tailored to the new environment, and not by making lots of changes in our code. That means we won't need to maintain multiple code bases, just multiple globals objects.
Those are some of the potential benefits. But there is more on the "costs" side of the equation than just the performance hit. Doing this will make our coding process just a little more complex. Also, we will run the the risk that our globals class name may collide with an identically named class from someone else who had the same idea.
You need to solve this equation for yourself. In the specific case of Random Image, I don't think the gained benefits count for a lot (chiefly because it's a core module and not likely to be used elsewhere) so I probably wouldn't use a globals object for this particular module.
Still, I'll build a globals object for the purpose of this article, so I can demonstrate how to build one and how to use it to make your module more testable. (I definitely wouldn't recommend using the same name for your globals object as I do; it's far too likely to trigger a collision.)
First, we'll walk through our code and locate all the static calls it makes into Joomla. These are the calls we can't mock, so can't test. We'll put each one of these into their own method in the globals class, with a name that identifies what they do.
We'll create a file called "JoomlaGlobals.php" and put it in the directory with the rest of the module code:
<?php /** * JoomlaGlobals * * This class abstracts all the calls into the Joomla API from the actual * module code. This means the module can be run completely isolated from * Joomla itself. */ class JoomlaGlobals { /** * getBaseURL * * Gets the domain (plus '/administrator' if on the admin site) of the site * URL * * return string */ public function getBaseURL() { return JURI::base(); } /** * strpos * * Calls a UTF-8 aware string search function * * @param needle string to be searched for * @param haystack string to search within * @param offset offset into haystack to start searching * * @return mixed Number of chars before first match, or false if not found */ public function strpos($needle, $haystack, $offset=false) { return JString::strpos($needle, $haystack, $offset); } }
These are the static calls triggered by calling getFolder. Now we add the globals class definition to the entry point file, and pass the globals object into our module object:
require_once dirname(__FILE__).'/JoomlaGlobals.php'; $randomImage = new modRandomImageHelper($params, new JoomlaGlobals);
Now we need to make our module object aware of the globals object:
/** * @var cms the globals object for talking to the cms */ protected $cms;
and:
public function __construct( $params, $cms ) { $this->params = $params; $this->cms = $cms; }
Now we're set up. We can change getImages back to a member function now, as it should be, and even make getFolder a member function as well:
public function getImages($theFolder, $type) { $folder = $this->getFolder($theFolder);
and:
public function getFolder($theFolder) { $folder = $theFolder; $LiveSite = $this->cms->getBaseURL(); // if folder includes livesite info, remove if ($this->cms->strpos($folder, $LiveSite) === 0) { $folder = str_replace($LiveSite, '', $folder); } // if folder includes absolute path, remove if ($this->cms->strpos($folder, JPATH_SITE) === 0) { $folder= str_replace(JPATH_BASE, '', $folder); } $folder = str_replace('\\', DIRECTORY_SEPARATOR, $folder); $folder = str_replace('/', DIRECTORY_SEPARATOR, $folder); return $folder; }
The static call JURI::base() has been replaced with $this->cms->getBaseURL(), a call into our globals object. Likewise with the later calls to JString. Under "normal" circumstances these calls will behave just like the static calls.
Now we need to make the test aware of the globals by putting this up with the rest of the class attributes:
/** * @var mock_globals The mock object for the CMS globals */ protected $mock_globals;
Yes, we're going to mock the globals in the setUp() method:
$this->mock_globals = $this->getMock('JoomlaGlobals', array('getBaseUrl', 'strpos')); $this->module = new modRandomImageHelper($this->params, $this->mock_globals);
We are telling PHPUnit to mock the calls to getBaseURL and strpos in our globals. We'll tell it what do about those calls later, in the test method.
You know that mockMe, late static binding, thing we put in earlier? We said at the time we were including it just to demonstrate how to use that technique. Since getImages() is no longer static, we can't use it, so we'll replace:
$mockMe = $this->getMockClass('modRandomImageHelper', array('getFolder')); $mockMe::staticExpects($this->any()) ->method('getFolder') ->with($this->equalTo($this->folder)) ->will($this->returnValue($this->folder)); $images = $mockMe::getImages($this->folder, $this->params->params['type'] = 'jpg');
with simply:
$images = $this->module->getImages($this->folder, $this->params->params['type'] = 'jpg');
This causes the testing of getImages() to by necessity include executing getFolder(). I'm going to leave it that way for the purposes of this article, which covers getting an existing module under test, and leave it as an exercise for the reader how to refactor either the test file or the module class (or both) to make the testing work best. (See the end of the article for more information on this).
Now we add the test code for getFolder:
/** * testGetFolder */ public function testGetFolder() { $this->mock_globals->expects($this->once()) ->method('getBaseURL') ->will($this->returnValue('http://www.testingsite.com')); $this->mock_globals->expects($this->exactly(2)) ->method('strpos') ->will($this->returnValue($this->folder)); $actual = $this->module->getFolder($this->folder); $this->assertEquals($actual, $this->folder); }
We start out by telling mock_globals we expect getBaseURL() to be called once, and we want it to return "http://www.testingsite.com" when it's called. Then we expect strpos() to be called twice, and to return the folder attribute each time it's called.
All that's left is to run the test on getFolder. To exercise more of the code, we will want to set up some test cases, as we did earlier, that will take different paths through the code. Again, I'll leave that as an exercise for the reader.
$ phpunit --bootstrap bootstrap.php suite PHPUnit 3.6.10 by Sebastian Bergmann. ............ Time: 0 seconds, Memory: 6.75Mb OK (12 tests, 31 assertions)
All that's left is to test the output. Looking in createOutput() I see two static calls, so we add those to the globals object:
/** * getTranslatedText * * Gets the text string in the current language * * @param string $string The string to translate. * @param mixed $jsSafe Boolean: Make the result javascript safe. * @param boolean $interpretBackSlashes To interpret backslashes (\\=\, \n=carriage return, \t=tabulation) * @param boolean $script To indicate that the string will be push in the javascript language store * * @return string The translated string or the key is $script is true */ public function getTranslatedText($string, $jsSafe = false, $interpretBackSlashes = true, $script = false) { return JText::_($string, $jsSafe, $interpretBackSlashes, $script); } /** * getLayoutPath * * Gets the text string in the current language * * @param string $layout The layout name to find. * * @return string The path to the layout file. */ public function getLayoutPath($layout) { return JModuleHelper::getLayoutPath('mod_random_image', $layout); }
Now we add the methods to be mocked to our list in the test file:
$this->mock_globals = $this->getMock('JoomlaGlobals', array('getBaseUrl', 'strpos', 'getTranslatedText', 'getLayoutPath', 'sendHTML') );
Hold on. What's that third mocked method, "sendHTML"? OK, you caught me. As long as we're editing this, I was slipping in something to use later. In the layout template there's a static call to JTEXT that also needs to be mocked, and I was preparing for that. No, there's no method in the "real" globals object called "sendHTML," but that's the beauty of mocks. You can mock what you need, and not be limited by what you have.
In order to use that mock, we need to create a test version of the default output template (we could make changes directly to the output template, but I wanted to demonstrate that the output template isn't needed until/unless we start testing it, specifically). We'll create a directory called "tmpl" in the fixtures directory, and create a file called "default.php" inside of that to be our test output template. We don't have to, be we will make it exactly like the "normal" default output template, except we'll substitute "$this->cms->sendHTML" for JTEXT::_ to direct the output towards our mock:
<?php // no direct access defined('_JEXEC') or die; ?> <div class="random-image<?php echo $moduleclass_sfx ?>"> <?php if ($link) : ?> <a href="/<?php echo $link; ?>"> <?php endif; ?> <?php echo $this->cms->sendHTML('image', $image->folder.'/'.$image->name, $image->name, array('width' => $image->width, 'height' => $image->height)); ?> <?php if ($link) : ?> </a> <?php endif; ?> </div>
Now we write our createOutput test:
/** * testCreateOutput */ public function testCreateOutput() { $image = '$lt;img src="/images/test.jpg" alt="test.jpg">'; $this->mock_globals->expects($this->any()) ->method('getTranslatedText') ->will($this->returnValue('No Images')); $this->mock_globals->expects($this->once()) ->method('getLayoutPath') ->will($this->returnValue( JPATH_BASE . '/tmpl/default.php' )); $this->mock_globals->expects($this->once()) ->method('sendHTML') ->will($this->returnValue($image)); ob_start(); $this->module->createOutput('default'); $view_output = ob_get_contents(); ob_end_clean(); $this->assertStringStartsWith('<div class="random-image">', $view_output); $this->assertStringEndsWith("</div>\n", $view_output); $this->assertTrue(!!strpos($view_output, $image)); }
This shows some simple ways to test this particular output. The tests you write here will be tailored to the output you want, and you can make them as abstract or as detailed as you wish. Just remember, the more detailed the test, the more likely you'll need to modify it if you modify the template.
For example, with the approach we took, if we were to modify the default template to not use a div, we'd need to come back to this test and change what it looks for.
Now, at this point on the road, we have:
$ phpunit --bootstrap bootstrap.php suite PHPUnit 3.6.10 by Sebastian Bergmann. ............. Time: 0 seconds, Memory: 6.75Mb OK (13 tests, 35 assertions)
Hopefully, as Joomla evolves, the static calls that made the testing difficult and the globals object necessary will go away, and you can one by one drop those calls from the globals object, until it withers away completely. Consider the object a crutch; quite useful when your leg is damaged, but able to be dispensed with after the patient heals.
As it is, we have a completely tested module, except for the globals object. The globals object should be set up to be tested separately from the module code so you can retest it easily every time Joomla changes, and it should be tested with Joomla, on a test installation. After all, what you're now testing with the globals object is whether Joomla has changed enough to cause your code problems. You don't need to retest your code, assuming you've tested it properly in the first place.
And since the module is completely testable, we can now add features and refactor the module code with confidence that what we're doing is sound.
The code for this article can be found in a GitHub repo and it's open. Fork it and send me a pull request if you have an improvement, and I'll be adding to it, as well. When I write articles like this I intentionally leave a few things open as exercises for the reader, and make a few sub-optimal (not wrong, just not best) choices. My intent is not to mislead, but to give you something to try your knowledge against, to prove to yourself you know how to use the tools. And, aside from the intentional choices I've made, I've no doubt there are unintentional mistakes as well. As can be seen from the links, the information here found its way to me from many different sources. The most useful have been PHPUnit itself, and the book "Working Effectively With Legacy Code" by Michael Feathers. I don't see how you can read that book and not be a better developer for doing so. It's on my "essentials" list.
Some articles published on the Joomla Community Magazine represent the personal opinion or experience of the Author on the specific topic and might not be aligned to the official position of the Joomla Project
By accepting you will be accessing a service provided by a third-party external to https://magazine.joomla.org/
Comments