Skip to content

Allow tests to fail if the page executes too many database queries #70

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Nov 14, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Annotations/QueryCount.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Liip\FunctionalTestBundle\Annotations;

/**
* @Annotation
* @Target({"METHOD"})
*/
class QueryCount
{
/** @var integer */
public $maxQueries;

public function __construct(array $values)
{
if (isset($values['value'])) {
$this->maxQueries = $values['value'];
}
}
}
34 changes: 34 additions & 0 deletions DependencyInjection/Compiler/SetTestClientPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Liip\FunctionalTestBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Alias;

class SetTestClientPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (null === $container->getParameter('liip_functional_test.query_count.max_query_count')) {
return;
}

if ($container->hasAlias('test.client')) {
// test.client is an alias.
// Register a private alias for this service to inject it as the parent
$container->setAlias(
'liip_functional_test.query_count.query_count_client.parent',
new Alias((string) $container->getAlias('test.client'), false)
);
} else {
// test.client is a definition.
// Register it again as a private service to inject it as the parent
$definition = $container->getDefinition('test.client');
$definition->setPublic(false);
$container->setDefinition('liip_functional_test.query_count.query_count_client.parent', $definition);
}

$container->setAlias('test.client', 'liip_functional_test.query_count.query_count_client');
}
}
7 changes: 7 additions & 0 deletions Exception/AllowedQueriesExceededException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace Liip\FunctionalTestBundle\Exception;

class AllowedQueriesExceededException extends \Exception
{
}
7 changes: 6 additions & 1 deletion LiipFunctionalTestBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@

use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Liip\FunctionalTestBundle\DependencyInjection\Compiler\SetTestClientPass;

class LiipFunctionalTestBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new SetTestClientPass());
}
}
32 changes: 32 additions & 0 deletions QueryCountClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Liip\FunctionalTestBundle;

use Symfony\Bundle\FrameworkBundle\Client;

class QueryCountClient extends Client
{
/** @var QueryCounter */
private $queryCounter;

public function setQueryCounter(QueryCounter $queryCounter)
{
$this->queryCounter = $queryCounter;
}

public function request(
$method,
$uri,
array $parameters = array(),
array $files = array(),
array $server = array(),
$content = null,
$changeHistory = true
) {
$crawler = parent::request($method, $uri, $parameters, $files, $server, $content, $changeHistory);

$this->queryCounter->checkQueryCount($this->getProfile()->getCollector('db')->getQueryCount());

return $crawler;
}
}
67 changes: 67 additions & 0 deletions QueryCounter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

namespace Liip\FunctionalTestBundle;

use Doctrine\Common\Annotations\Reader;
use Liip\FunctionalTestBundle\Annotations\QueryCount;
use Liip\FunctionalTestBundle\Exception\AllowedQueriesExceededException;

class QueryCounter
{
/** @var integer */
private $defaultMaxCount;

/** @var \Doctrine\Common\Annotations\AnnotationReader */
private $annotationReader;

public function __construct($defaultMaxCount, Reader $annotationReader)
{
$this->defaultMaxCount = $defaultMaxCount;
$this->annotationReader = $annotationReader;
}

public function checkQueryCount($actualQueryCount)
{
$maxQueryCount = $this->getMaxQueryCount();

if (null === $maxQueryCount) {
return;
}

if ($actualQueryCount > $maxQueryCount) {
throw new AllowedQueriesExceededException(
"Allowed amount of queries ($maxQueryCount) exceeded (actual: $actualQueryCount)."
);
}
}

private function getMaxQueryCount()
{
if ($maxQueryCount = $this->getMaxQueryAnnotation()) {
return $maxQueryCount;
}

return $this->defaultMaxCount;
}

private function getMaxQueryAnnotation()
{
foreach (debug_backtrace() as $step) {
if ('test' === substr($step['function'], 0, 4)) { //TODO: handle tests with the @test annotation
$annotations = $this->annotationReader->getMethodAnnotations(
new \ReflectionMethod($step['class'], $step['function'])
);

foreach ($annotations as $annotationClass) {
if ($annotationClass instanceof QueryCount AND isset($annotationClass->maxQueries)) {
/** @var $annotations \Liip\FunctionalTestBundle\Annotations\QueryCount */

return $annotationClass->maxQueries;
}
}
}
}

return false;
}
}
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,39 @@ execution silently continues, otherwise the calling test will fail and display a

setHtml5Wrapper:
Allow to change the default HTML5 code that is used as a wrapper around snippets to validate

Query Counter
=============
To catch pages that use way too many database queries, you can enable the query counter for tests. This will check the profiler for each request made in the test using the client, and fail the test if the number of queries executed is larger than the number of queries allowed in the configuration.
To enable the query counter, adjust the config_test.yml file, setting the liip_functional_test.query_count.max_query_count setting, like this:

liip_functional_test:
query_count.max_query_count: 50

That will limit each request executed within a functional test to 50 queries.

Maximum Query Count per Test
----------------------------
The default value set in the config file should be reasonable to catch pages with high query counts which are obviously mistakes. There will be cases where you know and accept that the request will cause a large number of queries, or where you want to specifically require the page to execute less than x queries, regardless of the amount set in the configuration. For those cases you can set an annotation on the test method that will override the default maximum for any requests made in that test.

To do that, include the Liip\FunctionalTestBundle\Annotations\QueryCount namespace and add the `@QueryCount(100)` annotation, where 100 is the maximum amount of queries allowed for each request, like this:

use Liip\FunctionalTestBundle\Annotations\QueryCount;

class DemoTest extends WebTestCase
{
/**
* @QueryCount(100)
*/
public function testDoDemoStuff()
{
$client = static::createClient();
$crawler = $client->request('GET', '/demoPage');

$this->assertTrue($crawler->filter('html:contains("Demo")')->count() > 0);
}
}

Caveats
-------
* QueryCount annotations currently only work for tests that have a method name of testFooBla() (with a test prefix). The @test annotation isn't supported at the moment.
20 changes: 20 additions & 0 deletions Resources/config/functional_test.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,25 @@
<parameter key="password"></parameter>
</parameter>

<parameter key="liip_functional_test.query_count.max_query_count">null</parameter>

</parameters>

<services>
<service id="liip_functional_test.query_count.query_count_client" class="Liip\FunctionalTestBundle\QueryCountClient" scope="prototype">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm .. i keep wondering if there is a better pattern to be able to add things on top of a service definition and then alias the new version to the old service name ...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<argument type="service" id="kernel" />
<argument>%test.client.parameters%</argument>
<argument type="service" id="test.client.history" />
<argument type="service" id="test.client.cookiejar" />

<call method="setQueryCounter">
<argument type="service" id="liip_functional_test.query_count.query_counter" />
</call>
</service>

<service id="liip_functional_test.query_count.query_counter" class="Liip\FunctionalTestBundle\QueryCounter">
<argument>%liip_functional_test.query_count.max_query_count%</argument>
<argument type="service" id="annotation_reader" />
</service>
</services>
</container>
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
],
"require": {
"php": ">=5.3.2",
"symfony/framework-bundle": ">=2.0,<2.3-dev"
"symfony/framework-bundle": ">=2.0,<2.3-dev",
"doctrine/common": "2.*"
},
"autoload": {
"psr-0": {
Expand Down