Testy jednostkowe: jak zamockować funkcję wbudowaną

Ostatnio w pracy pisałem kawałek kodu PHP. Test jednostkowy był jednak kłopotliwy, gdyż wymagał zamockowania funkcji php_sapi_name(), a więc wbudowanej funkcji PHP, a nie elementu klasy, którą w całości mógłbym wrzucić w Mockery. Oto jak sobie z tym poradziłem.

Kłopotliwość polegała na tym, że test PHPUnit wywoływany jest zawsze z poziomu konsoli, a pisana przeze mnie klasa miała zachowywać się różnie w zależności od tego, czy PHP był wołany z CLI, czy jako proces FastCGI. Różnych zachowań kodu nie dało się zatem przetestować polegając na realnym zachowaniu funkcji php_sapi_name().

To jednak rodzi kolejny problem: Mockery nie nadpisze funkcji wbudowanej, gdyż służy jedynie do tworzenia zamockowanych instancji obiektów. Jednocześnie nie da się po prostu nadpisać funkcji, gdyż wywoła to błąd „cannot redeclare function”. Co zatem robić?

Bardzo pomocny okazał się fakt, że pisany przeze mnie kod używał przestrzeni nazw. Przestrzenie nazw mają taką fajną właściwość, że funkcje i klasy, do których się odwołujemy bez podania pełnej przestrzeni nazw (bez odwróconego ukośnika z przodu) są domyślnie brane z aktualnej przestrzeni nazw, a dopiero w razie ich braku pobierane są z innych. A zatem jeśli mamy następujący kod:

namespace My\Awesome\Namezpace;
class MyClass {
    public function myFunction () {
        return php_sapi_name();
    }
}

to pierwszym, co sprawdzi PHP podczas wywoływania funkcji php_sapi_name() będzie, czy należy do przestrzeni nazw My\Awesome\Namezpace. Zatem jeśli chcemy w teście jednostkowym zamockować tę funkcję bez wywoływania błędu, musimy umieścić ją w tej przestrzeni nazw, którą interpreter PHP przeszuka. Plik z naszym testem mógłby więc wyglądać tak:

namespace {
    $sapi = "cli";
}
namespace My\Awesome\Namezpace {
    function php_sapi_name () {
        global $sapi;
        return $sapi;
    }
}
namespace My\Awesome\Namezpace\Test {
    use My\Awesome\Namezpace\MyClass;
    class MyClassTest extends \PHPUnit_Framework_TestCase {
        private $myClass;

        public function setUp () {
            $this->myClass = new MyClass();
        }

        public function testMyFunctionCli () {
            $this->assertEquals("cli",$this->myClass->myFunction());
        }

        public function testMyFunctionApache () {
            global $sapi;
            $sapi = "apache";
            $this->assertEquals("apache",$this->myClass->myFunction());
        }
    }
}

Komentarze

Mike

2013-12-24, 22:29

Zamiast wymyślać i korzystać z "hacków" (?), rozsądniejsze wydaje się opakowanie funkcji w klasę i korzystanie w taki sposób, zamiast bezpośrednio za pomocą funkcji natywnej.

Dominik Marczuk

2013-12-24, 22:58

Sposób działania jest zasadniczo ten sam: manipulujemy przestrzenią nazw :). Masz rację, że takie coś byłoby wygodne, bo w teście jednostkowym możnaby temat załatwić dwoma linijkami kodu (Mockery i po sprawie), ale martwi mnie trochę fakt, że zabieg obniża wydajność działania całej aplikacji tylko i wyłącznie na potrzeby wygody programisty piszącego test jednostkowy. Testy są po to, aby zapewnić działanie aplikacji, a nie na odwrót. Mimo wszystko wielkie dzięki za podrzucenie innej możliwości :).

Cezary Draus

2014-04-14, 18:17

Drugą opcją w stosunku do opcji Mike jest opakowanie metodą, która nie robi nic innego jak zwraca rezultat funkcji wbudowanej... Możesz podmienić w mocku metodę opakowującą funkcję wbudowaną i testować mocka... Generalnie używanie stanu globalnego podczas testowania narusza zasady izolacji. Wyobraź sobie, że testowany obiekt nadpisuje z jakiegoś powodu $sapi albo inne testy gdzieś w systemie (np. nie Twoje) również korzystają z tej zmiennej globalnej.

Dominik Marczuk

2014-05-23, 01:19

Cezary, zasadniczo zarówno Mike, jak i Ty macie rację, że testy lepiej by się miały, gdyby funkcja była opakowana w jakąś metodę - czy to w oddzielnej klasie, czy np. w traicie (bo przecież może się zdarzyć, że jest więcej klas, które z takiej funkcji wbudowanej chcą skorzystać). Izolacja faktycznie nie jest zachowana w moim przykładzie, a dodam też, że csfixer po takim czymś to by najchętniej przeleciał halabardą (deklaracja funkcji bez akcesora jest niezgodna z PSR-2, a csfixer nie patrzy, czy ta funkcja jest wewnątrz jakiejś klasy), ale moim priorytetem było niemodyfikowanie kodu samej aplikacji w żaden sposób i mimo wszystko zamockowanie funkcji :).


Napisz komentarz


Szukaj wpisów


Chmura tagów