J'ai un scénario intéressant dans le sens où j'ai besoin d'une fonction à définir pour pouvoir tester une autre fonction. La fonction que je veux tester ressemble à ceci:
if (function_exists('foo') && ! function_exists('baz')) {
/**
* Baz function
*
* @param integer $n
* @return integer
*/
function baz($n)
{
return foo() + $n;
}
}
La raison pour laquelle je vérifie l'existence de foo
est qu'il peut être défini ou non dans le projet d'un développeur et que la fonction baz
s'appuie sur foo
. Pour cette raison, je veux seulement que baz
soit défini s'il peut appeler foo
.
Le seul problème est que jusqu'à présent, il était impossible d'écrire des tests. J'ai essayé de créer un script d'amorçage dans la configuration de PHPUnit qui définirait une fausse fonction foo
et exigerait ensuite l'autochargeur Composer, mais mon script principal pense toujours que foo
n'est pas défini. foo
n'est pas un package Composer et ne peut par ailleurs être requis par mon projet. Manifestement, Mockery ne fonctionnera pas pour cela non plus. Ma question est de savoir si quelqu'un de plus expérimenté avec PHPUnit a rencontré ce problème et trouvé une solution.
Merci!
Commencez par un léger remaniement du code pour le rendre plus testable.
function conditionalDefine($baseFunctionName, $defineFunctionName)
{
if(function_exists($baseFunctionName) && ! function_exists($defineFunctionName))
{
eval("function $defineFunctionName(\$n) { return $baseFunctionName() + \$n; }");
}
}
Alors appelez ça comme ça:
conditionalDefine('foo', 'bar');
Votre classe de test PHPUnit contiendra les tests suivants:
public function testFunctionIsDefined()
{
$baseName = $this->mockBaseFunction(3);
$expectedName = uniqid('testDefinedFunc');
conditionalDefine($baseName, $expectedName);
$this->assertTrue(function_exists($expectedName));
$this->assertEquals(5, $expectedName(2));
}
public function testFunctionIsNotDefinedBecauseItExists()
{
$baseName = $this->mockBaseFunction(3);
$expectedName = $this->mockBaseFunction($value = 'predefined');
conditionalDefine($base, $expectedName);
// these are optional, you can't override a func in PHP
// so all that is necessary is a call to conditionalDefine and if it doesn't
// error, you're in the clear
$this->assertTrue(function_exists($expectedName));
$this->assertEquals($value, $expectedName());
}
public function testFunctionIsNotDefinedBecauseBaseFunctionDoesNotExists()
{
$baseName = uniqid('testBaseFunc');
$expectedName = uniqid('testDefinedFunc');
conditionalDefine($base, $expectedName);
$this->assertFalse(function_exists($expectedName));
}
protected function mockBaseFunction($returnValue)
{
$name = uniqid('testBaseFunc');
eval("function $name() { return '$value'; }");
return $name;
}
Cela suffit pour ce que vous demandez. Vous pouvez cependant, plus loin refactoriser ce code en extrayant la génération de fonction dans un générateur de code. C'est ce que vous pouvez écrire des tests unitaires sur le générateur pour vous assurer qu'il crée le type de fonction que vous attendez.
Cela devrait marcher!
use MyProject\baz;
class YourTestCase
{
/** @var callable **/
protected $mockFoo;
/** @var callable **/
protected $fakeFoo;
public function setUp()
{
if (function_exists('foo')) {
$this->mockFoo = function($foosParams) {
foo($foosParams);
// Extra Stuff, as needed to make the test function right.
};
}
$this->fakeFoo = function($foosParams) {
// Completely mock out foo.
};
}
public function testBazWithRealFoo()
{
if (!$this->mockFoo) {
$this->markTestIncomplete('This system does not have the "\Foo" function.');
}
$actualResults = baz($n, $this->mockFoo);
$this->assertEquals('...', $actualResults);
}
public function testBazWithMyFoo()
{
$actualResults = baz($n, $this->fakeFoo);
$this->assertEquals('...', $actualResults);
}
}
Puis modifiez votre code existant:
if (function_exists('foo') && ! function_exists('baz')) {
/**
* Baz function
*
* @param integer $n
* @return integer
*/
function baz($n)
{
return foo() + $n;
}
}
namespace MyProject
{
function baz($bazParams, callable $foo = '\foo')
{
return $foo() + $bazParams;
}
}
Ensuite, au lieu d’appeler baz($n)
, vous devez appeler:
use MyProject\baz;
baz($bazParams);
C'est comme l'injection de dépendance pour les fonctions, yo ;-)
Il semble que ce soit une situation dans laquelle vous devez vous moquer de la visibilité, ce qui est clairement en dehors du domaine de PHPUnit, une bibliothèque de classes. Vous pouvez donc toujours utiliser PHPUnit, mais vous devrez peut-être vous en sortir pour atteindre votre objectif.
Le seul moyen auquel je puisse penser pour créer une maquette d'une fonction est directement dans le scénario de test, au-dessus de l'objet qui étend\PHPUnit_Framework_TestCase.
function foo() {
return 1;
}
if (function_exists('foo') && ! function_exists('baz')) {
/**
* Baz function
*
* @param integer $n
* @return integer
*/
function baz($n)
{
return foo() + $n;
}
}
class TestBaz extends \PHPUnit_Framework_TestCase
{
public function test_it() {
$this->assertSame(4,baz(3));
}
}
Vous devrez peut-être créer deux fichiers, un avec foo et un sans, ou quatre si vous voulez tester foo/not foo et baz/not baz. C’est définitivement une option hors du commun, que je ne recommanderais pas normalement, mais dans votre cas, c’est peut-être votre meilleur choix.
Est-ce suffisant?
to-test.php:
<?php
if (function_exists('foo') && ! function_exists('baz')) {
/**
* Baz function
*
* @param integer $n
* @return integer
*/
function baz($n)
{
return foo() + $n;
}
}
BazTest.php:
<?php
class BazTest extends PHPUnit_Framework_TestCase {
public function setUp()
{
function foo()
{
// Appropriate mock goes here
return 1;
}
include __DIR__ . '/to-test.php';
}
public function testBaz()
{
$this->assertEquals(2, baz(1));
$this->assertEquals(3, baz(2));
}
}
Lancer le test donne:
PHPUnit 5.4.8 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 54 ms, Memory: 8.00MB
OK (1 test, 2 assertions)
L'ajouter dans le fichier d'amorçage ne fonctionne pas - pourquoi?
Est-ce parce que parfois la fonction baz
est (A) correctement créée par le système et parfois (B) vous devez vous en moquer? Ou (C) avez-vous toujours besoin de vous moquer de cela?
Je suis sûr que vous avez correctement configuré votre amorçage phpunit, mais pour faire bonne mesure, est-ce que cela ressemble à quelque chose comme:
/tests/phpunit.xml
:
<phpunit
bootstrap="phpunit.bootstrap.php"
</phpunit>
/tests/phpunit.bootstrap.php
:
<?php
require(__DIR__ . "/../bootstrap.php"); // Application startup logic; this is where the function "baz" gets defined, if it exists
if (function_exists('foo') && ! function_exists('baz')) {
/**
* Baz function
*
* @param integer $n
* @return integer
*/
function baz($n)
{
return foo() + $n;
}
}
Ne créez pas la fonction baz
à la volée dans vos tests, par exemple. dans une fonction setUp
.
Les suites de tests dans phpunit utilisent le même programme d'amorçage. Par conséquent, si vous devez tester des cas dans lesquels la fonction baz
est définie et d'autres cas dans lesquels elle n'est pas définie (et que vous devez la simuler), vous devez fractionner le dossier tests
, par exemple. dans deux dossiers différents, chacun avec leurs fichiers phpunit.xml
et phpunit.bootstrap.php
. Par exemple. /tests/with-baz
et /tests/mock-baz
. À partir de ces deux dossiers, exécutez les tests séparément. Créez simplement des liens symboliques vers la variable phpunit
dans chaque sous-dossier (par exemple, à partir de /test/with-baz
, créez ln -s ../../vendor/bin/phpunit
si compositeur est à la racine) pour vous assurer d’exécuter la même version de phpunit dans les deux scénarios.
La solution ultime consiste bien entendu à déterminer où la fonction baz
est définie et à inclure manuellement le fichier de script incriminé, dans la mesure du possible, pour garantir l'application de la logique correcte.
Alternative
Utilisez l'annotation @runInSeparateProcess
de phpunit et définissez la fonction selon vos besoins.
<?php
class SomeTest extends \PHPUnit_Framework_TestCase
{
/**
* @runInSeparateProcess
*/
public function testOne()
{
if (false === function_exists('baz')) {
function baz() {
return 42;
}
}
$this->assertSame(42, baz());
}
public function testTwo()
{
$this->assertFalse(function_exists('baz'));
}
}