Unit tests rarely come to mind when talking about performance management. This is for a good reason: tests depending on time lead to transient failures. Blackfire lets developers write tests on stable metrics that are related to the root causes of performance issues instead of time. Using such metrics in a unit test suite is a powerful option.
To make things more concrete, let's write some unit tests for the Finding Bigfoot
App\Twig\AppExtension::getUserActivityText()
method we
optimized previously. The test suite already has a test
covering the output of the method:
1 2 3 4 5 6 7
public function testGetUserActivityText()
{
$user = $this->createMock(User::class);
$activityText = $this->appExtension->getUserActivityText($user);
$this->assertNotEmpty($activityText);
$this->assertIsString($activityText);
}
But how can we test a SQL query is only executed the very first time
getUserActivityText()
is called for a specific User
?
Have a look at the constructor and calculateUserActivityText()
method
implementation and note the usage of the
Symfony\Contracts\Cache\CacheInterface
class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Component\Cache\CacheItem;
public function __construct(CommentHelper $commentHelper, CacheInterface $cache)
{
$this->commentHelper = $commentHelper;
$this->cache = $cache;
}
public function getUserActivityText(User $user): string
{
$key = sprintf('user_activity_text_%s', $user->getId());
return $this->cache->get($key, function (CacheItem $item) use ($user) {
$item->expiresAfter(3600);
return $this->calculateUserActivityText($user);
});
}
As PHPUnit cannot mock the CacheInterface
instance, we need to find another way:
CacheInterface
(extending the
real one and wrapping the get()
method) before running any tests and
register it before Composer's autoloader kicks in. That's rather ugly,
fragile, and confusing to do it by hand but some mocking libraries can help
you achieve that.Let's see how Blackfire integrates with PHPUnit. The easiest way is to include
the Blackfire\Bridge\Phpunit\TestCaseTrait
trait.
1 2 3 4 5 6 7
use Blackfire\Bridge\PhpUnit\TestCaseTrait as BlackfireTrait;
use PHPUnit\Framework\TestCase;
class AppExtensionTest extends TestCase
{
use BlackfireTrait;
}
Then, create a PHPUnit test like this one:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
use Blackfire\Profile\Configuration as ProfileConfiguration;
public function testIsUserActivityTextCached()
{
$config = new ProfileConfiguration();
$config->assert('metrics.sql.queries.count == 1', 'One SQL query');
$this->assertBlackfire($config, function () {
$user = $this->createMock(User::class);
$this->getUserActivityText($user);
$this->getUserActivityText($user);
$this->getUserActivityText($user);
});
}
The assertBlackfire()
call is where the magic happens:
metrics.sql.queries.count == 1
);Be sure to add this test before any other tests; if not, the cache could be already warmed and the test would fail.
If the test fails, PHPUnit displays a nice error message with all the information you need to debug it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
F.
Time: 3.03 seconds, Memory: 15.00Mb
There was 1 failure:
1) App\Tests\Twig\AppExtensionTest::testIsUserActivityTextCached
Failed asserting that Blackfire tests pass.
1 tests failures out of 1.
failed: One SQL query
- metrics.sql.queries.count == 1
More information at https://blackfire.io/profiles/b58d2c0c-5870-42bd-ad7d-5392bd30b966/graph?settings%5BtabPane%5D=assertions.
/var/www/html/vendor/blackfire/php-sdk/src/Blackfire/Bridge/PhpUnit/TestConstraint.php:60
/var/www/html/vendor/blackfire/php-sdk/src/Blackfire/Bridge/PhpUnit/TestConstraint71.php:19
/var/www/html/vendor/blackfire/php-sdk/src/Blackfire/Bridge/PhpUnit/TestCaseTrait.php:53
/var/www/html/tests/Twig/AppExtensionTest.php:36
FAILURES!
Tests: 2, Assertions: 1, Failures: 1.
If cache is commented out, the assertion fails as 3 processes are run. The call graph URL points to the profile where you can debug problems more easily.
If the assertion has a syntax error, the profile "Assertions" tab helps you understand the problem:
This test was easy to write because we were able to use a built-in metric, but this is not always the case. Luckily, it is possible to create custom metrics with the PHP SDK:
1 2 3 4 5 6 7
use Blackfire\Profile\Configuration as ProfileConfiguration;
use Blackfire\Profile\Metric;
$config = new ProfileConfiguration();
$config->defineMetric(new Metric( 'user_activity_text', '=App\Twig\AppExtension::getUserActivityText'));
$config->assert('metrics.sql.queries.count == 1', 'One SQL query');
Read our PHPUnit integration documentation for more tips on how to leverage Blackfire in PHPUnit test suites.
Blackfire lets you test your code's behavior without the need for mocks. This is a very useful technique when you need to test the behavior of a third-party library you cannot easily modify. With Blackfire there is no need to change third-party code or write an ugly hack. Simple define custom metrics and write assertions against them.
Using Blackfire in a PHPUnit test has one great side-effect: your performance tests automatically benefit from your continuous integration setup. Automation is key for continuously managing performance, and this is the topic for the next few chapters.