Jak testowanie wzorca rejestru lub Singletona jest trudne w PHP?

Dlaczego testowanie singletons lub registry pattern jest trudne w języku takim jak PHP, który jest napędzany żądaniami?

Możesz pisać i uruchamiać testy poza faktycznym wykonaniem programu, dzięki czemu możesz swobodnie wpływać na globalny stan programu i uruchamiać kilka przerw i inicjalizacji dla każdej funkcji testowej, aby uzyskać ten sam stan dla każdego testu.

Czy coś przeoczyłem?

Author: Peter Mortensen, 2011-03-12

3 answers

Chociaż prawdą jest, że "możesz pisać i uruchamiać testy poza faktycznym wykonaniem programu, dzięki czemu możesz swobodnie wpływać na globalny stan programu i uruchamiać kilka przerw i inicjalizacji dla każdej funkcji testowej, aby uzyskać ten sam stan dla każdego testu."[25], jest to żmudne, aby to zrobić. Chcesz przetestować TestSubject w izolacji i nie tracić czasu na odtwarzanie środowiska pracy.

Przykład

class MyTestSubject
{
    protected $registry;

    public function __construct()
    {
        $this->registry = Registry::getInstance();
    }
    public function foo($id)
    {
        return $this->doSomethingWithResults(
            $registry->get('MyActiveRecord')->findById($id)
        );
    }
}

Aby to zadziałało, musisz mieć Beton Registry. Jest zakodowany i Singleton. Ten ostatni oznacza zapobieganie wszelkim skutkom ubocznym z poprzedniego badania. Musi być zresetowany dla każdego testu, który zostanie uruchomiony na MyTestSubject. Możesz dodać metodę Registry::reset() i wywołać ją w setup(), ale dodanie metody tylko po to, aby móc przetestować wydaje się brzydkie. Załóżmy, że i tak potrzebujesz tej metody, więc kończysz z

public function setup()
{
    Registry::reset();
    $this->testSubject = new MyTestSubject;
}

Teraz nadal nie masz obiektu 'MyActiveRecord', w którym ma on powrócić foo. Ponieważ lubisz rejestr, Twój MyActiveRecord faktycznie wygląda tak

class MyActiveRecord
{
    protected $db;

    public function __construct()
    {
        $registry = Registry::getInstance();
        $this->db = $registry->get('db');
    }
    public function findById($id) { … }
}

W konstruktorze MyActiveRecord znajduje się kolejne wywołanie do rejestru. Test musi się upewnić, że coś zawiera, w przeciwnym razie test się nie powiedzie. Oczywiście, nasza klasa baz danych również jest Singletonem i musi zostać zresetowana między testami. Doh!

public function setup()
{
    Registry::reset();
    Db::reset();
    Registry::set('db', Db::getInstance('host', 'user', 'pass', 'db'));
    Registry::set('MyActiveRecord', new MyActiveRecord);
    $this->testSubject = new MyTestSubject;
}

Więc z tymi w końcu skonfigurowanymi, możesz zrobić swój test

public function testFooDoesSomethingToQueryResults()
{
    $this->assertSame('expectedResult', $this->testSubject->findById(1));
}

I zdaj sobie sprawę, że masz jeszcze inną zależność: twoja baza danych testów fizycznych nie została jeszcze skonfigurowana. Podczas gdy ty zakładając bazę testową i wypełniając ją danymi, przyszedł twój szef i powiedział ci, że idziesz SOA teraz i wszystkie te połączenia z bazą danych muszą być zastąpionepołączeniami z serwisem internetowym .

Istnieje do tego nowa klasa MyWebService i musisz sprawić, że MyActiveRecord użyje tego zamiast tego. Świetnie, właśnie tego potrzebowałeś. Teraz musisz zmienić wszystkie testy, które używają bazy danych. Cholera, myślisz. Całe to gówno, żeby upewnić się, że doSomethingWithResults działa zgodnie z oczekiwaniami? Nie obchodzi mnie, skąd pochodzą dane.

Introducing mocks

Dobra wiadomość jest taka, że możesz zastąpić wszystkie zależności przez stubowanie lub mock them. Podwójny test będzie udawał, że jest prawdziwy.

$mock = $this->getMock('MyWebservice');
$mock->expects($this->once())
     ->method('findById')
     ->with($this->equalTo(1))
     ->will($this->returnValue('Expected Unprocessed Data'));

Spowoduje to utworzenie podwójnego dla usługi internetowej, która oczekuje, że zostanie wywołana raz podczas testu Z pierwszym argumentem metody findById będąc 1. It will return predefiniowane data.

Po umieszczeniu tego w metodzie w swojej walizce testowej, Twoje setup staje się

public function setup()
{
    Registry::reset();
    Registry::set('MyWebservice', $this->getWebserviceMock());
    $this->testSubject = new MyTestSubject;
}
Świetnie. Nie musisz już martwić się o ustawienie prawdziwego środowiska teraz. Poza rejestrem. To też może być kpina. Ale jak to zrobić. Jest zakodowany na twardo, więc nie ma sposobu na zastąpienie go w środowisku testowym. Cholera!

Ale chwileczkę, czy nie powiedzieliśmy właśnie, że MyTestClass nie obchodzi, skąd pochodzą dane? Tak, obchodzi mnie tylko to, że może wywołać metodę findById. Ty mam nadzieję, że pomyśl teraz: dlaczego rejestr jest tam w ogóle? I masz rację. Zmieńmy całość na

class MyTestSubject
{
    protected $finder;

    public function __construct(Finder $finder)
    {
        $this->finder = $finder;
    }
    public function foo($id)
    {
        return $this->doSomethingWithResults(
            $this->finder->findById($id)
        );
    }
}

ByeBye Registry. Teraz wstrzykujemy zależność MyWebSe ... Err ... Finder?! Tak. Po prostu zależy nam na metodzie findById, więc teraz używamy interfejsu

interface Finder
{
    public function findById($id);
}

Nie zapomnij odpowiednio zmienić makiety

$mock = $this->getMock('Finder');
$mock->expects($this->once())
     ->method('findById')
     ->with($this->equalTo(1))
     ->will($this->returnValue('Expected Unprocessed Data'));

I setup() staje się

public function setup()
{
    $this->testSubject = new MyTestSubject($this->getFinderMock());
}
Voila! Spokojnie i spokojnie. Teraz możemy skupić się na testowaniu klasy mytest.

While robiłaś to, twój szef zadzwonił ponownie i powiedział, że chce, żebyś przełączył się z powrotem do bazy danych, ponieważ SOA to tak naprawdę tylko hasło używane przez drogich konsultantów, aby poczuć przedsiębiorczość. Tym razem jednak nie martw się, ponieważ nie musisz ponownie zmieniać testów. Nie zależą już od środowiska.

Oczywiście nadal musisz upewnić się, że zarówno MyWebservice, jak i MyActiveRecord implementują interfejs Findera dla Twojego rzeczywistego kodu, ale ponieważ założyliśmy je aby mieć już te metody, to tylko kwestia spoliczkowania implements Finder na klasie.

I tyle. Mam nadzieję, że to pomogło.

Dodatkowe Zasoby:

Możesz znaleźć dodatkowe informacje o innych wadach podczas testowania singletonów i radzenia sobie ze stanem globalnym w

To powinno być najbardziej interesujące, ponieważ jest to autor PHPUnit i wyjaśnia trudności z rzeczywiste przykłady w PHPUnit.

Interesujące są również:

 35
Author: Gordon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-03-13 06:29:05

Singletony (we wszystkich językach OOP, nie tylko PHP) utrudniają debugowanie nazywane testowaniem jednostkowym z tego samego powodu, co zmienne globalne. Wprowadzają one stan globalny do programu, co oznacza, że nie można testować żadnych modułów oprogramowania, które zależą od Singletona w izolacji. Testowanie jednostkowe powinno obejmować tylko testowany kod (i jego superklasy).

Singletony są zasadniczo stanem globalnym, a posiadanie stanu globalnego może mieć sens w w pewnych okolicznościach należy tego unikać, chyba że jest to konieczne.

 5
Author: GordonM,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-03-12 14:35:52

Po zakończeniu testu PHP, możesz spłukać instancję Singletona w następujący sposób:

protected function tearDown()
{
    $reflection = new ReflectionClass('MySingleton');
    $property = $reflection->getProperty("_instance");
    $property->setAccessible(true);
    $property->setValue(null);
}
 2
Author: Taofather,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-07-15 11:12:19