Pussy: Refactoring. Teil zwei oder Suchtbehandlung

Ursprünglicher Autor: Matthias Noback
  • Übersetzung
  • Tutorial
BildDiese Übersetzung ist eine Fortsetzung einer Reihe von Refactoring-Artikeln von Matthias Noback.

Die Welt ist nicht so zuverlässig, dass sie sich darauf verlassen kann


Während des Komponententests muss die externe Umgebung nicht in den Testprozess selbst einbezogen werden. Wenn Sie echte Datenbankabfragen, HTTP-Anforderungen oder das Schreiben in Dateien ausführen, verlangsamen Sie die Tests, da diese Vorgänge nicht vorhersehbar sind. Wenn beispielsweise der Server, an den Sie während des Tests Anforderungen richten, abstürzt oder nicht optimal reagiert, wird der Komponententest unterbrochen, auch wenn alles andere ordnungsgemäß funktioniert. Dies ist schlecht, da Komponententests nur abstürzen sollten, wenn der Code etwas tut, was er nicht tun sollte.

Wie Sie im letzten Artikel sehen können, hängen beide Klassen (CachedCatApi und RealCatApi) von externen Faktoren ab. Der erste schreibt Dateien in das Dateisystem, der zweite stellt echte HTTP-Anforderungen, während diese Momente eher untergeordnet sind und die richtigen Tools nicht für sie verwendet werden. Darüber hinaus wird eine große Anzahl von Grenzfällen in diesen Klassen nicht berücksichtigt.

Beide Klassen können solcher Abhängigkeiten beraubt werden, und dafür ist es ausreichend, dass die neuen Klassen alle diese Details auf niedriger Ebene einkapseln. Beispielsweise können wir den Aufruf von file_get_contents () in einer anderen Klasse mit dem Namen FileGetContentsHttpClient leicht entfernen.

class FileGetContentsHttpClient
{
    public function get($url)
    {
        return @file_get_contents($url);
    }
}

Abhängigkeitsumkehrung wieder


Wie im vorherigen Artikel können Sie nicht einfach ein Stück Code nehmen und an eine andere Klasse übertragen. Für eine neue Klasse müssen Sie eine Schnittstelle eingeben, da es ohne diese schwierig sein wird, einen normalen Test zu schreiben:

interface HttpClient
{
    /**
     * @return string|false Response body
     */
    public function get($url);
}

Jetzt können Sie HttpClient als Argument an den RealCatApi-Konstruktor übergeben:

class RealCatApi implements CatAPi
{
    private $httpClient;
    public function __construct(HttpClient $httpClient)
    {
        $this->httpClient = $httpClient;
    }
    public function getRandomImage()
    {
        $responseXml = $this->httpClient->get('http://thecatapi.com/api/images/get?format=xml&type=jpg');
        ...
    }
}

Echter Unit Test


Ab sofort gibt es einen coolen Unit-Test für RealCatApi. Alles, was Sie tun müssen, ist, den HttpClient zu ersetzen (Stand-in?), Damit er eine vordefinierte XML-Antwort zurückgibt:

class RealCatApiTest extends \PHPUnit_Framework_TestCase
{
    /** @test */
    public function it_fetches_a_random_url_of_a_cat_gif()
    {
        $xmlResponse = <<http://24.media.tumblr.com/tumblr_lgg3m9tRY41qgnva2o1_500.jpgbiehttp://thecatapi.com/?id=bie

EOD;
        $httpClient = $this->getMock('HttpClient');
        $httpClient
            ->expect($this->once())
            ->method('get')
            ->with('http://thecatapi.com/api/images/get?format=xml&type=jpg')
            ->will($this->returnValue($xmlResponse));
        $catApi = new RealCatApi($httpClient);
        $url = $catApi->getRandomImage();
        $this->assertSame(
            'http://24.media.tumblr.com/tumblr_lgg3m9tRY41qgnva2o1_500.jpg',
            $url
        );
    }
}

Dies ist nun der richtige Test, der das folgende RealCatApi-Verhalten überprüft: Es sollte HttpClient mit einer bestimmten URL aufrufen und den Feldwert aus der XML-Antwort zurückgeben.

Trennen Sie die API von file_get_contents ()


Eine weitere Sache muss noch behoben werden: Die Methode get () der Klasse HttpClient hängt immer noch vom Verhalten von file_get_contents () ab, dh sie gibt false zurück, wenn die Anforderung nicht erfolgreich war, oder den Antworttext als Zeichenfolge, wenn die Anforderung erfolgreich war. Wir können dieses Implementierungsdetail leicht verbergen, indem wir einige zurückgegebene Werte (wie beispielsweise false) in die für sie definierten Ausnahmen konvertieren (benutzerdefinierte Ausführung). Daher begrenzen wir die Anzahl der verarbeiteten Objekte, die unsere Objekte passieren, streng. In unserem Fall ist dies nur ein Funktionsargument, eine zurückgegebene Zeichenfolge oder eine Ausnahme:

class FileGetContentsHttpClient implements HttpClient
{
    public function get($url)
    {
        $response = @file_get_contents($url);
        if ($response === false) {
            throw new HttpRequestFailed();
        }
        return $response;
    }
}
interface HttpClient
{
    /**
     * @return string Response body
     * @throws HttpRequestFailed
     */
    public function get($url);
}
class HttpRequestFailed extends \RuntimeException
{
}

RealCatApi muss noch leicht geändert werden, damit Ausnahmen abgefangen werden können, anstatt auf false zu reagieren:

class RealCatApi implements CatAPi
{
    public function getRandomImage()
    {
        try {
            $responseXml = $this->httpClient->get('http://thecatapi.com/api/images/get?format=xml&type=jpg');
            ...
        } catch (HttpRequestFailed $exception) {
            return 'http://cdn.my-cool-website.com/default.jpg';
        }
        ...
    }
}

Haben Sie das bemerkt, bevor wir nur die richtige Adresse getestet haben? Wir haben nur das erfolgreiche Ergebnis von file_get_contents () mit einer gültigen XML-Antwort getestet. Es war nicht möglich, die gestürzte HTTP-Anfrage zu testen, da nicht klar ist, wie Sie die HTTP-Anfrage zwangsweise "fluten" können, außer durch Herausziehen des Netzwerkkabels.

Jetzt haben wir die volle Kontrolle über HttpClient und können einen Anforderungsabsturz simulieren - werfen Sie einfach eine HttpRequestFailed-Ausnahme aus:

class RealCatApiTest extends \PHPUnit_Framework_TestCase
{
    ...
    /** @test */
    public function it_returns_a_default_url_when_the_http_request_fails()
    {
        $httpClient = $this->getMock('HttpClient');
        $httpClient
            ->expect($this->once())
            ->method('get')
            ->with('http://thecatapi.com/api/images/get?format=xml&type=jpg')
            ->will($this->throwException(new HttpRequestFailed());
        $catApi = new RealCatApi($httpClient);
        $url = $catApi->getRandomImage();
        $this->assertSame(
            'http://cdn.my-cool-website.com/default.jpg',
            $url
        );
    }
}

Das Dateisystem loswerden


Wir können die gleichen Schritte für CachedCatApi je nach Dateisystem wiederholen:

interface Cache
{
    public function isNotFresh($lifetime);
    public function put($url);
    public function get();
}
class FileCache implements Cache
{
    private $cacheFilePath;
    public function __construct()
    {
        $this->cacheFilePath = __DIR__ . '/../../cache/random';
    }
    public function isNotFresh($lifetime)
    {
        return !file_exists($this->cacheFilePath) 
                || time() - filemtime($this->cacheFilePath) > $lifetime
    }
    public function put($url)
    {
        file_put_contents($this->cacheFilePath, $url);
    }
    public function get()
    {
         return file_get_contents($this->cacheFilePath);
    }
}
class CachedCatApi implements CatApi
{
    ...
    private $cache;
    public function __construct(CatApi $realCatApi, Cache $cache)
    {
        ...
        $this->cache = $cache;
    }
    public function getRandomImage()
    {
        if ($this->cache->isNotFresh()) {
            ...
            $this->cache->put($url);
            return $url;
        }
        return $this->cache->get();
    }
}

Endlich können wir diese furchterregenden sleep () - Aufrufe in CachedCatApiTest loswerden! Und all dies liegt an der Tatsache, dass wir einen einfachen Wrapper für Cache haben. Ich werde diesen Teil als eigenständige Übung für den Leser belassen.

Es gab mehrere Probleme:

  1. Ich mag die Cache-Interface-API nicht. Die Methode isNotFresh () ist schwer zu lesen. Es entspricht auch nicht den existierenden Abstraktionen (zum Beispiel aus Doctrine), was es für Leute, die mit Caching in PHP vertraut sind, unverständlich macht.
  2. Der Cache-Pfad ist in der FileCache-Klasse weiterhin fest codiert. Dies ist schlecht für das Testen - es gibt keine Möglichkeit, dies zu ändern.

Die erste kann gelöst werden, indem einige Methoden umbenannt und eine boolesche Logik invertiert werden. Das zweite Problem wird gelöst, indem der erforderliche Pfad als Argument an den Konstruktor übergeben wird.

Fazit


In diesem Teil haben wir viele Details auf niedriger Ebene im Zusammenhang mit dem Dateisystem und HTTP-Anforderungen verborgen. Auf diese Weise können Sie wirklich korrekte Komponententests schreiben.

Natürlich muss der Code in FileCache und FileGetContentsHttpClient noch getestet werden, der Artikel endet und die Tests sind immer noch langsam und fragil. Sie können dies jedoch tun: Weigern Sie sich, sie zu testen, und verwenden Sie keine vorhandenen Lösungen für die Arbeit mit Dateien oder die Ausführung von HTTP-Anforderungen . Das Testen solcher Bibliotheken liegt ausschließlich bei den Entwicklern. Auf diese Weise können Sie sich auf die wichtigen Teile Ihres Codes konzentrieren und Ihre Tests beschleunigen.

UPD : Pussy: Refactoring. Teil Drei oder Kammrauheit

Jetzt auch beliebt: