Friday, October 19, 2012

Testing shell methods with PHPUnit

Testing is fun. Testing global functions isn't, especially when those global functions use pass-by-reference parameters.

I write a lot of command line applications with PHP and I often must deal with exec, shell_exec, and passthru. This causes major headaches when I need to simulate a behavior for the purposes of testing.

In Bart, we have a class named Shell, which wraps many of the global shell and system functions of the PHP language. It's also a great place to collect any methods that don't come out of the Box with PHP. Combining the Shell class with Diesel  lets me mock or stub out pretty much all of my interactions with the shell. There's one catch though: PHPUnit doesn't work with mocking methods that have pass-by-reference parameters. Both exec and passthru return information back to the caller via this approach.

Enter the MockShell class. MockShell is a small stub class exposing an exec and passthru method. It also exposes two other methods to configure the commands expected by either of these methods for the duration of a unit test. Finally, it provides a verify() method to be called upon completion of a test run to verify that its expectations were met.

MockShell takes a mocked Shell class instance as a parameter. Any method calls sent to the MockShell that it doesn't understand are passed along the mocked MockShell. This utility of this is to allow you to provide only one stubbed object when you configure Diesel for the test. See below,

function testSymlinksCreated()
{
 $phpuShell = $this->getMock('Bart\Shell');
 $phpuShell->expects($this->exactly(1))
  ->method('mkdir')
  ->with('~/code/nagios/logs', 0777, false);

 // Create the MockShell and supply it the mocked Shell instance
 // ...so that any calls to methods _other_ than exec and passthru
 // ...may be passed on to that mock.
 $shell = new \Bart\Stub\MockShell($this, $phpuShell);
 $shell
  ->expectExec("cd ~/code/nagios && ln -s /etc/nagios config", array(), 0, null)
  ->expectExec("cd /www && ln -s ~/code/nagios nagios", array(), 0, null);

 $this->registerDiesel('Bart\Shell', $shell);

 // Configure a generator for the nagios app
 $g = new Generator('nagios');
 // Expect that this creates the "logs" directory
 // ...and creates the two symlinks above
 $g->generateDirs();

 // Verify that the two execs were called
 // PHPUnit will verify the call to mkdir()
 $shell->verify();
}

Check out all the Bart code at, http://github.com/box/bart.

1 comment:

  1. Check php-mock which can mock global functions like exec().

    ReplyDelete