Kann ich die Zeit in PHPUnit „verspotten“?

74

... nicht zu wissen, ob 'mock' das richtige Wort ist.

Wie auch immer, ich habe eine geerbte Codebasis, für die ich einige Tests schreiben möchte, die zeitbasiert sind. Um nicht zu vage zu sein, bezieht sich der Code darauf, den Verlauf eines Elements zu betrachten und festzustellen, ob dieses Element jetzt einen Zeitschwellenwert hat.

Irgendwann muss ich auch testen, ob ich etwas zu diesem Verlauf hinzufüge und überprüfe, ob der Schwellenwert jetzt geändert wurde (und natürlich korrekt ist).

Das Problem, auf das ich stoße, ist, dass ein Teil des Codes, den ich teste, Aufrufe von time () verwendet. Daher fällt es mir wirklich schwer, genau zu wissen, wie hoch die Schwellenzeit sein sollte, basierend auf der Tatsache, dass ich ' Ich bin mir nicht ganz sicher, wann genau diese time () -Funktion aufgerufen wird.

Meine Frage lautet also im Grunde: Gibt es eine Möglichkeit für mich, den Aufruf von time () zu "überschreiben" oder die Zeit irgendwie zu "verspotten", sodass meine Tests in einer "bekannten Zeit" funktionieren?

Oder muss ich einfach akzeptieren, dass ich in dem Code, den ich teste, etwas tun muss, damit ich ihn bei Bedarf zwingen kann, eine bestimmte Zeit zu verwenden?

Gibt es in beiden Fällen gängige Vorgehensweisen für die Entwicklung zeitkritischer Funktionen, die testfreundlich sind?

Bearbeiten: Ein Teil meines Problems ist auch die Tatsache, dass die Zeit, zu der Dinge in der Geschichte auftraten, die Schwelle beeinflusst. Hier ist ein Beispiel für einen Teil meines Problems ...

Stellen Sie sich vor, Sie haben eine Banane und versuchen herauszufinden, wann sie gegessen werden muss. Nehmen wir an, dass es innerhalb von 3 Tagen abläuft, es sei denn, es wurde mit einer Chemikalie besprüht. In diesem Fall addieren wir 4 Tage zum Verfallsdatum ab dem Zeitpunkt , an dem das Spray angewendet wurde . Dann können wir weitere 3 Monate hinzufügen, indem wir es einfrieren. Wenn es jedoch gefroren ist, haben wir nur 1 Tag Zeit, um es nach dem Auftauen zu verwenden.

Alle diese Regeln werden von historischen Zeitpunkten bestimmt. Ich bin damit einverstanden, dass ich den Vorschlag des Dominik, innerhalb weniger Sekunden zu testen, nutzen kann, aber was ist mit meinen historischen Daten? Sollte ich das einfach im laufenden Betrieb "erstellen"?

Wie Sie vielleicht oder vielleicht nicht sagen können, versuche ich immer noch, all dieses 'Test'-Konzept in den Griff zu bekommen;)

Narzisse
quelle
Für PHP7 könnten Sie github.com/runkit7/Timecop-PHP verwenden, das auf runkit7 basiert
Alex

Antworten:

61

Ich habe kürzlich eine andere Lösung gefunden, die großartig ist, wenn Sie PHP 5.3-Namespaces verwenden. Sie können eine neue time () - Funktion in Ihrem aktuellen Namespace implementieren und eine gemeinsam genutzte Ressource erstellen, in der Sie den Rückgabewert in Ihren Tests festlegen. Dann verwendet jeder unqualifizierte Aufruf von time () Ihre neue Funktion.

Zur weiteren Lektüre habe ich es in meinem Blog ausführlich beschrieben

Fabian Schmengler
quelle
1
Ich mag diese Idee wirklich, Fabian. Ein zusätzlicher Vorteil ist, dass es meine Arbeitskollegen zwingt, auf 5.3 zu aktualisieren;)
Narcissus
Dies ist äußerst nützlich. Vielen Dank, dass Sie diese Technik mit uns teilen. Fabian - sehr geschätzt!
MicE
geniale Arbeit hier. Namespaces für Funktionen machen es nützlich, integrierte Funktionen zum Testen zu ersetzen.
Mauris
2
Ich habe kürzlich die Bibliothek php-mock implementiert, die diese Sprachfunktion zum Verspotten nicht deterministischer PHP-Funktionen wie verwendet time().
Markus Malkusch
2
Tolle Lösung. Selbst wenn sich Ihr SUT in einem anderen Namespace als Ihr Test befindet, können Sie es verwenden, indem Sie "mehrere Namespaces in derselben Datei" verwenden. Php.net/manual/en/language.namespaces.definitionmultiple.php
antonienko
7

Für diejenigen von Ihnen mit symfony arbeiten (> = 2.8): Symfony der PHPUnit - Brücke enthält eine ClockMock - Funktion , die die integrierten Methoden überschreibt time, microtime, sleepund usleep.

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

simon.ro
quelle
Danke, das hilft sehr!
mblaettermann
6

Haftungsausschluss: Ich habe diese Bibliothek geschrieben.

Sie können die Testzeit mit Clock von Ouzo-Goodies verspotten .

Verwenden Sie im Code einfach:

$time = Clock::now();

Dann in Tests:

Clock::freeze('2014-01-07 12:34');
$result = Class::getCurrDate();
$this->assertEquals('2014-01-07', $result);
Piotr Olaszewski
quelle
31
Wenn Sie auf Ihre eigene Software verlinken, sollten Sie einen Haftungsausschluss hinzufügen, der alle darüber informiert, dass Sie ihn geschrieben haben.
Will
5

Ich musste eine bestimmte Anfrage in Zukunft und in der Vergangenheit in der App selbst simulieren (nicht in Unit Tests). Daher sollten alle Aufrufe von \ DateTime :: now () das Datum zurückgeben, das zuvor in der gesamten App festgelegt wurde.

Ich habe mich für diese Bibliothek https://github.com/rezzza/TimeTraveler entschieden , da ich die Daten verspotten kann, ohne alle Codes zu ändern.

\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
SenG
quelle
Es scheint cool für diejenigen, die ein new \DateTime()in ihrem Code verwenden. Aber wie soll das installiert werden? Keine Infos im Github Repo.
Kekko12
3

Persönlich verwende ich weiterhin time () in den getesteten Funktionen / Methoden. Stellen Sie in Ihrem Testcode nur sicher, dass Sie nicht auf Gleichheit mit time () testen, sondern nur auf einen Zeitunterschied von weniger als 1 oder 2 (je nachdem, wie viel Zeit die Funktion für die Ausführung benötigt).

Dominik
quelle
Im Moment sieht es so aus, als müsste ich so vorgehen. Ich habe meinem Problem auch ein "Beispiel" hinzugefügt, falls dies hilft. Vielen Dank.
Narzisse
Ich habe dein Beispiel gesehen. Auch hier verwende ich für diese Art von Tests die phpunit-Setup-Methode, um die 'korrekten' historischen Daten (zum Beispiel in der Datenbank) vorzubereiten
Dominik
1
Dies macht Ihre Tests sehr zerbrechlich. Sie können anscheinend ohne Grund fehlschlagen, wenn der zu testende Prozess eine Verzögerung erfährt (aus welchem ​​Grund auch immer).
t.heintz
3

Carbon::setTestNow(Carbon $time = null)ruft zur gleichen Zeit an Carbon::now()oder new Carbon('now')kehrt zurück.

https://medium.com/@stefanledin/mock-date-and-time-with-carbon-8a9f72cb843d

Beispiel:

    public function testSomething()
    {
        $now = Carbon::now();
        // Mock Carbon::now() / new Carbon('now') to always return the same time
        Carbon::setTestNow($now);

        // Do the time sensitive test:
        $this->retroEncabulator('prefabulate')
            ->assertJsonFragment(['whenDidThisHappen' => $now->timestamp])

        // Release the Carbon::now() mock
        Carbon::setTestNow();
    }

Die $this->retroEncabulator()Funktion muss natürlich Carbon::now()oder new Carbon('now')intern verwendet werden.

Henk Poley
quelle
Vielleicht können Sie es so verwenden,$now = Carbon::now(); Carbon::setTestNow($now);
mohammad.kaab
@ mohammad.kaab Ich habe ein Beispiel hinzugefügt, das genau das tut 👍
Henk Poley
2

Sie können die time () - Funktion von php mit der runkit-Erweiterung überschreiben. Stellen Sie sicher, dass Sie runkit.internal_overide auf On setzen

Vlad Balmos
quelle
2

Verwenden der Erweiterung [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');
}

Sie können den Mock sogar so testen:

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();
}
jhvaras
quelle
2

In den meisten Fällen reicht dies aus. Es hat einige Vorteile:

  • du musst dich über nichts lustig machen
  • Sie benötigen keine externen Plugins
  • Sie können jede Zeitfunktion verwenden, nicht nur time (), sondern auch DateTime-Objekte
  • Sie müssen keine Namespaces verwenden.

Es wird phpunit verwendet, aber Sie können es an jedes andere Testframework anpassen. Sie benötigen lediglich eine Funktion, die wie assertContains () von phpunit funktioniert.

1) Fügen Sie Ihrer Testklasse oder Ihrem Bootstrap die folgende Funktion hinzu. Die Standardtoleranz für die Zeit beträgt 2 Sekunden. Sie können es ändern, indem Sie das dritte Argument an assertTimeEquals übergeben oder Funktionsargumente ändern.

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

2) Testbeispiel:

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

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

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

assertTimeEquals () prüft, ob das Array von (189, 190, 191) 189 enthält.

Dieser Test sollte für die korrekte Arbeitsfunktion bestanden werden, wenn die Ausführung der Testfunktion weniger als 2 Sekunden dauert.

Es ist nicht perfekt und sehr genau, aber es ist sehr einfach und in vielen Fällen reicht es aus, um zu testen, was Sie testen möchten.

Konrad Gałęzowski
quelle
1

Die einfachste Lösung wäre, die PHP time () -Funktion zu überschreiben und durch Ihre eigene Version zu ersetzen. Sie können integrierte PHP-Funktionen jedoch nicht einfach ersetzen ( siehe hier ).

Abgesehen davon besteht die einzige Möglichkeit darin, den Aufruf von time () an eine eigene Klasse / Funktion zu abstrahieren, die die zum Testen benötigte Zeit zurückgibt.

Alternativ können Sie das Testsystem (Betriebssystem) in einer virtuellen Maschine ausführen und die Zeit des gesamten virtuellen Computers ändern.

Milan Babuškov
quelle
Danke Milan: Ich stimme zwar zu, dass das Ausführen in einer VM eine Option wäre, um die Zeit zu erzwingen, aber ich denke, ich müsste immer noch die "Laufzeitvariablen" berücksichtigen und am Ende immer noch das tun, was Dominik vorgeschlagen hat. Interessante Idee, danke.
Narzisse
1

Hier ist eine Ergänzung zu Fabs Beitrag. Ich habe die Namespace-basierte Überschreibung mit einer Auswertung durchgeführt. Auf diese Weise kann ich es nur für Tests ausführen und nicht den Rest meines Codes. Ich führe eine ähnliche Funktion aus:

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

Übergeben Sie dann timeOverrides(array(...))das Test-Setup, damit meine Tests nur verfolgen müssen, in welchen Namespaces time () aufgerufen wird.

Photis
quelle