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;)


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


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


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);

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

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).


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.


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


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

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.


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.


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.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.