web-dev-qa-db-fra.com

Puis-je "simuler" le temps dans PHPUnit?

... ne sachant pas si "simuler" est le bon mot.

Quoi qu'il en soit, j'ai une base de code héritée pour laquelle j'essaie d'écrire des tests qui sont basés sur le temps. En essayant de ne pas être trop vague, le code est lié à la consultation de l'historique d'un élément et à la détermination si cet élément a désormais basé un seuil de temps.

À un moment donné, je dois également tester l'ajout de quelque chose à cet historique et vérifier que le seuil est maintenant modifié (et, évidemment, correct).

Le problème que je frappe est qu'une partie du code que je teste utilise des appels à time () et donc je trouve vraiment difficile de savoir exactement quel devrait être le temps seuil, basé sur le fait que je ' Je ne sais pas exactement quand cette fonction time () va être appelée.

Donc, ma question est essentiellement la suivante: y a-t-il un moyen pour moi de "remplacer" l'appel time (), ou en quelque sorte de "simuler" l'heure, de sorte que mes tests fonctionnent à une "heure connue"?

Ou dois-je simplement accepter le fait que je vais devoir faire quelque chose dans le code que je teste, pour me permettre de le forcer à utiliser un moment particulier si besoin est?

Quoi qu'il en soit, existe-t-il des "pratiques courantes" pour développer des fonctionnalités sensibles au temps qui sont faciles à tester?

Edit: Une partie de mon problème, aussi, est le fait que le moment où les choses se sont produites dans l'histoire affecte le seuil. Voici un exemple d'une partie de mon problème ...

Imaginez que vous avez une banane et que vous essayez de déterminer quand elle doit être mangée. Disons qu'il expirera dans les 3 jours, sauf s'il a été pulvérisé avec un produit chimique, auquel cas nous ajoutons 4 jours à l'expiration, à partir du moment où le spray a été appliqué. Ensuite, nous pouvons y ajouter 3 mois supplémentaires en le congelant, mais s'il a été congelé, nous n'avons qu'un jour pour l'utiliser après sa décongélation.

Toutes ces règles sont dictées par des horaires historiques. Je suis d'accord pour pouvoir utiliser la suggestion de Dominik de tester en quelques secondes, mais qu'en est-il de mes données historiques? Dois-je simplement "créer" cela à la volée?

Comme vous pouvez ou ne pouvez pas le dire, j'essaie toujours de comprendre tout ce concept de `` test '';)

69
Narcissus

J'ai récemment trouvé une autre solution qui est géniale si vous utilisez PHP 5.3 espaces de noms. Vous pouvez implémenter une nouvelle fonction time () dans votre espace de noms actuel et créer une ressource partagée où vous définissez le retour Dans vos tests, tout appel non qualifié à time () utilisera votre nouvelle fonction.

Pour une lecture plus approfondie, je l'ai décrit en détail dans mon blog

58
Fabian Schmengler

Avertissement: j'ai écrit cette bibliothèque.

Vous pouvez simuler le temps de test en utilisant Clock from ouzo-goodies .

Dans le code, utilisez simplement:

$time = Clock::now();

Puis dans les tests:

Clock::freeze('2014-01-07 12:34');
$result = Class::getCurrDate();
$this->assertEquals('2014-01-07', $result);
6
Piotr Olaszewski

Pour ceux d'entre vous qui travaillent avec symfony (> = 2.8): le pont PHPUnit de Symfony comprend une fonction ClockMock qui remplace les méthodes intégrées time, microtime, sleep et usleep.

Voir: http://symfony.com/doc/2.8/components/phpunit_bridge.html#clock-mocking

5
simon.ro

J'ai dû simuler une demande particulière à une date future et passée dans l'application elle-même (pas dans les tests unitaires). Par conséquent, tous les appels à\DateTime :: now () doivent renvoyer la date précédemment définie dans l'application.

J'ai décidé d'aller avec cette bibliothèque https://github.com/rezzza/TimeTraveler , car je peux me moquer des dates sans modifier tous les codes.

\Rezzza\TimeTraveler::enable();
\Rezzza\TimeTraveler::moveTo('2011-06-10 11:00:00');

var_dump(new \DateTime());           // 2011-06-10 11:00:00
var_dump(new \DateTime('+2 hours')); // 2011-06-10 13:00:00
3
SenG

Personnellement, je continue à utiliser time () dans les fonctions/méthodes testées. Dans votre code de test, assurez-vous de ne pas tester l'égalité avec time (), mais simplement pour une différence de temps inférieure à 1 ou 2 (selon le temps nécessaire à l'exécution de la fonction)

3
Dominik

Dans la plupart des cas, cela suffira. Il présente certains avantages:

  • vous n'avez rien à vous moquer
  • vous n'avez pas besoin de plugins externes
  • vous pouvez utiliser n'importe quelle fonction de temps, non seulement des objets time () mais aussi DateTime
  • vous n'avez pas besoin d'utiliser des espaces de noms.

Il utilise phpunit, mais vous pouvez l'adapter à n'importe quel autre framework de test, vous avez juste besoin d'une fonction qui fonctionne comme assertContains () de phpunit.

1) Ajoutez la fonction ci-dessous à votre classe de test ou bootstrap. La tolérance par défaut pour le temps est de 2 secondes. Vous pouvez le changer en passant le 3e argument à assertTimeEquals ou modifier les arguments de la fonction.

private function assertTimeEquals($testedTime, $shouldBeTime, $timeTolerance = 2)
{
    $toleranceRange = range($shouldBeTime, $shouldBeTime+$timeTolerance);
    return $this->assertContains($testedTime, $toleranceRange);
}

2) Exemple de test:

public function testGetLastLogDateInSecondsAgo()
{
    // given
    $date = new DateTime();
    $date->modify('-189 seconds');

    // when
    $this->setLastLogDate($date);

    // then
    $this->assertTimeEquals(189, $this->userData->getLastLogDateInSecondsAgo());
}

assertTimeEquals () vérifiera si le tableau de (189, 190, 191) contient 189.

Ce test doit être réussi pour une fonction de travail correcte SI l'exécution de la fonction de test prend moins de 2 secondes.

Ce n'est pas parfait et super précis, mais c'est très simple et dans de nombreux cas, c'est suffisant pour tester ce que vous voulez tester.

2
Konrad Gałęzowski

Utilisation de l'extension [runkit] [1]:

define('MOCK_DATE', '2014-01-08');
define('MOCK_TIME', '17:30:00');
define('MOCK_DATETIME', MOCK_DATE.' '.MOCK_TIME);

private function mockDate()
{
    runkit_function_rename('date', 'date_real');
    runkit_function_add('date','$format="Y-m-d H:i:s", $timestamp=NULL', '$ts = $timestamp ? $timestamp : strtotime(MOCK_DATETIME); return date_real($format, $ts);');
}


private function unmockDate()
{
    runkit_function_remove('date');
    runkit_function_rename('date_real', 'date');
}

Vous pouvez même tester la maquette comme ceci:

public function testMockDate()
{
    $this->mockDate();
    $this->assertEquals(MOCK_DATE, date('Y-m-d'));
    $this->assertEquals(MOCK_TIME, date('H:i:s'));
    $this->assertEquals(MOCK_DATETIME, date());
    $this->unmockDate();
}
2
jhvaras

Vous pouvez remplacer la fonction php time () en utilisant l'extension runkit. Assurez-vous de définir runkit.internal override sur On

2
Vlad Balmos

Voici un ajout au post de fab. J'ai fait le remplacement basé sur l'espace de noms en utilisant un eval. De cette façon, je peux simplement l'exécuter pour des tests et non le reste de mon code. J'exécute une fonction similaire à:

function timeOverrides($namespaces = array()) {
  $returnTime = time();
  foreach ($namespaces as $namespace) {
    eval("namespace $namespace; function time() { return $returnTime; }");
  }
}

passez ensuite timeOverrides(array(...)) dans la configuration de test afin que mes tests n'aient qu'à garder une trace de quels espaces de noms time () est appelé.

1
Photis

La solution la plus simple serait de remplacer la fonction PHP time () et de la remplacer par votre propre version. Cependant, vous ne pouvez pas remplacer les fonctions intégrées PHP les fonctions facilement (- voir ici ).

En dehors de cela, la seule façon est d'abstraire l'appel time () à une classe/fonction de votre choix qui retournerait le temps dont vous avez besoin pour les tests.

Vous pouvez également exécuter le système de test (système d'exploitation) sur une machine virtuelle et modifier l'heure de l'ensemble de l'ordinateur virtuel.

1
Milan Babuškov