Testing PHP code more efficiently

Article by Tommy Mühle
17
Jan

Testing PHP code more efficiently

Quality code is highly valued in the PHP community. You’ll rarely find untested libraries on GitHub. Two problems that developers encounter again and again during testing are the handling of file operations, as well as testing built-in PHP functions such as time() or exec() on certain expectations. In this article, Tommy Mühle explains a few solutions for such cases.

As a PHP developer, you can easily get into the situation of working with or interacting with files, especially when processing uploaded files or writing cache or log files. And as a good developer, you also want to cover this program flow with tests.

Listing 1 shows a simplified example of a CacheWarmer class that creates a cache directory in the warmUp method if it does not already exist.

<?php namespace My\App; class CacheWarmer { private $cacheDirectory; public function __construct(string $cacheDirectory) { $this->cacheDirectory = $cacheDirectory;
  }

  public function warmUp()
  {
    if (!is_dir($this->cacheDirectory) &&!mkdir($this->cacheDirectory)) {
      throw new \RuntimeException('Could not create cache directory!');
    }
    // ...
  }
}

If you want to test such a class with the help of a unit test, the simplest approach would be to do it directly in the file system. The class is initialized locally, the warmUp method is called, and the system checks at the end whether the corresponding directory has been created and correctly filled. Although this method is quick and extremely effective, it does involve a variety of possible problems. For example, if you are working with an operating system other than the actual target system, the testers and developers must keep this in mind. It is also important to make sure that the tests clean up the file system correctly afterwards – on the one hand to avoid side effects in dependent tests and on the other hand to prevent your own file system from being flooded with test data.

The better solution to check such applications is to use a virtual file system. A virtual file system is created as a separate stream wrapper for PHP. This allows built-in PHP functions such as mkdir or file_exists to be used. The vfsStream library offers just that. One advantage is that vfsStream can be used with almost any PHP test framework, such as PHPUnit. The installation is done as usual with Composer:

composer require –dev mikey179/vfsStream

Now, the file system to be tested is only streamed and discarded after the tests have been completed. This has the big advantage that you don’t have to worry about cleaning up and side effects of tests. Better performance for I/O operations can also play a role in large test suites. Listing 2 shows a possible test of our CacheWarmer class using PHPUnit and vfsStream.

<?php namespace My\App\Tests; use My\App\CacheWarmer; use org\bovigo\vfs\vfsStream; use PHPUnit\Framework\TestCase; class CacheWarmerTest extends TestCase { private $root; public function setUp() { $this->root = vfsStream::setup();
    }

    /**
     * @test
     */
    public function canCreateCacheDirectoryOnWarmUp()
    {
        $cacheWarmer = new CacheWarmer($this->root->url() . '/cache');
        $cacheWarmer->warmUp();

        $this->assertTrue($this->root->hasChild('cache'));
    }
}

The library has many other useful features such as permission handling and adjustable disk quotas. However, there are also limitations, which should be considered. For example, interaction with symlinks is not possible. Further documentation and examples can be found in the official Wiki of the project.

Testing built-in PHP functions

Another problem you often encounter as a PHP developer is working with built-in PHP functions like time() or exec(). These are usually difficult to test. Listing 3 shows a simplified version of a PdfCreator class that uses wkhtmltopdf  internally to create PDFs and executes it via the exec() function.

<?php
namespace My\App;

class PdfCreator
{
    // ...

    public function execute(string $htmlFile, string $pdfFile)
    {
        $output = [];
        $returnValue = 0;

        exec(sprintf('/usr/bin/wkhtmltopdf %s %s', $htmlFile, $pdfFile), $output, $returnValue);

        if ($returnValue !== 0) {
            throw new \RuntimeException('Could not create PDF file!');
        }

        return $output;
    }

    // ...
}

Of course, this procedure can be bypassed with an appropriate software architecture. However, this is not always possible. If you still want to make sure that the application handles return values of built-in PHP functions correctly, there are several ways to do this.

Unit instead of integration tests

The first, and often obvious, variant would be to cover this with a functional or integration test. In this case, you would execute the application directly under certain conditions or contexts and check the results accordingly. However, these tests are often very time-consuming because the test environment must be prepared or initialized each time.

To test the expectations of built-in PHP functions directly, we can use php-mock. This tool also supports various test frameworks like PHPUnit or Prophecy (phpspec). To mock built-in PHP functions php-mock uses the namespace fallback rule of PHP. It says that used built-in PHP functions are first searched in your own namespace if they are set unqualified (without leading backslash). Only after that the function from the global namespace is taken.

The version of php-mock for PHPUnit can also be installed via Composer:

composer require –dev php-mock/php-mock-phpunit

For the tests php-mock provides a trait, so that a possible test for our already shown PdfCreator class could look like Listing 4.

<?php namespace My\App\Tests; use My\App\PdfCreator; use PHPUnit\Framework\TestCase; class PdfCreatorTest extends TestCase { use \phpmock\phpunit\PHPMock; /** * @test * @expectedException \RuntimeException */ public function executeReturnsAnExceptionOnFailure() { $exec = $this->getFunctionMock('My\App', 'exec');
        $exec->expects($this->once())->willReturnCallback(
            function ($command, &$output, &$returnValue) {
                $this->assertEquals('/usr/bin/wkhtmltopdf file.html file.pdf', $command);
                $output = ['failure'];
                $returnValue = 1;
            }
        );

        $pdfCreator = new PdfCreator;
        $pdfCreator->execute('file.html', 'file.pdf');
    }
}

Such simple unit tests obviously have big advantages. But the downside is that you have to know exactly which return values are possible. Otherwise, errors may occur despite extensive test coverage. A further small disadvantage is the already mentioned limitation to unqualified function calls. But those can be set easily in your own code.

Conclusion and outlook

Sometimes the most obvious test procedures at first glance are the ones with the biggest pitfalls. This is especially true when working with extensive test suites. In this context, the two libraries presented here are useful as real efficiency helpers when it comes to avoiding slow integration tests and replacing them with unit tests. For this reason, a developer should always keep this option in mind.

Stay tuned!

Behind the Tracks of IPC ’18

PHP Development
Best Practices & Application

Web Development
Web Development & more

Web Architecture
Concepts & Environments

Performance & Security
All about Performance & Security

Agile & DevOps
Agile & DevOps methodologies

JavaScript
All about JavaScript

Testing & Quality
All about Testing & Quality