Najlepsze praktyki testowania chronionych metod za pomocą PHPUnit [closed]

Znalazłem dyskusję na temat Czy testujesz prywatną metodę pouczającą.

Zdecydowałem, że w niektórych klasach chcę mieć metody chronione, ale je przetestować. Niektóre z tych metod są statyczne i krótkie. Ponieważ większość publicznych metod z nich korzysta, prawdopodobnie będę mógł bezpiecznie usunąć testy później. Ale na początek podejście TDD i uniknąć debugowania, naprawdę chcę je przetestować.

Pomyślałem:

  • metoda Obiekt {[2] } zgodnie z informacją w odpowiedź wydaje się być przesadą.
  • zacznij od metod publicznych i gdy zasięg kodu jest podany przez testy wyższego poziomu, włącz je jako chronione i usuń testy.
  • dziedziczenie klasy z testowalnym interfejsem czyniącym metody chronione publicznymi

Jaka jest najlepsza praktyka? Coś jeszcze?

Wydaje się, że JUnit automatycznie zmienia chronione metody na publiczne, ale nie przyjrzałem się temu głębiej. PHP robi nie pozwól na to poprzez odbicie .

Author: Community, 2008-10-30

8 answers

Jeśli używasz PHP5 (>= 5.3.2) z PHPUnit, możesz przetestować swoje prywatne i chronione metody używając reflection, aby ustawić je jako publiczne przed uruchomieniem testów:

protected static function getMethod($name) {
  $class = new ReflectionClass('MyClass');
  $method = $class->getMethod($name);
  $method->setAccessible(true);
  return $method;
}

public function testFoo() {
  $foo = self::getMethod('foo');
  $obj = new MyClass();
  $foo->invokeArgs($obj, array(...));
  ...
}
 368
Author: uckelman,
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
2010-10-24 10:49:41

Wydaje się, że już wiesz, ale i tak to powtórzę; to zły znak, jeśli chcesz przetestować chronione metody. Celem testu jednostkowego jest przetestowanie interfejsu klasy, A chronione metody są szczegółami implementacji. To powiedziawszy, są przypadki, w których ma to sens. Jeśli używasz dziedziczenia, możesz zobaczyć superklasę jako interfejs dla podklasy. W tym przypadku trzeba by przetestować metodę protected (ale nigdy metodę prywatną ). Rozwiązaniem tego jest Utwórz podklasę do celów testowych i użyj jej do ujawnienia metod. Np.:

class Foo {
  protected function stuff() {
    // secret stuff, you want to test
  }
}

class SubFoo extends Foo {
  public function exposedStuff() {
    return $this->stuff();
  }
}

Zauważ, że zawsze możesz zastąpić dziedziczenie kompozycją. Podczas testowania kodu zwykle dużo łatwiej jest poradzić sobie z kodem, który używa tego wzorca, więc warto rozważyć tę opcję.

 41
Author: troelskn,
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
2008-10-30 10:46:19

Teastburn ma właściwe podejście. Jeszcze prostsze jest bezpośrednie wywołanie metody i zwrócenie odpowiedzi:

class PHPUnitUtil
{
  public static function callMethod($obj, $name, array $args) {
        $class = new \ReflectionClass($obj);
        $method = $class->getMethod($name);
        $method->setAccessible(true);
        return $method->invokeArgs($obj, $args);
    }
}

Możesz to nazwać po prostu w swoich testach przez:

$returnVal = PHPUnitUtil::callMethod(
                $this->object,
                '_nameOfProtectedMethod', 
                array($arg1, $arg2)
             );
 27
Author: robert.egginton,
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
2017-05-23 12:10:41

Chciałbym zaproponować lekką odmianę getMethod () zdefiniowaną w odpowiedzi uckelmana .

Ta wersja zmienia getMethod (), usuwając zakodowane wartości i nieco upraszczając użycie. Polecam dodać go do klasy PHPUnitUtil jak w przykładzie poniżej lub do klasy phpunit_framework_testcase-extending (lub, jak przypuszczam, globalnie do pliku PHPUnitUtil).

Ponieważ MyClass i tak jest tworzona instancja, a ReflectionClass może przyjmować ciąg znaków lub obiekt...

class PHPUnitUtil {
    /**
     * Get a private or protected method for testing/documentation purposes.
     * How to use for MyClass->foo():
     *      $cls = new MyClass();
     *      $foo = PHPUnitUtil::getPrivateMethod($cls, 'foo');
     *      $foo->invoke($cls, $...);
     * @param object $obj The instantiated instance of your class
     * @param string $name The name of your private/protected method
     * @return ReflectionMethod The method you asked for
     */
    public static function getPrivateMethod($obj, $name) {
      $class = new ReflectionClass($obj);
      $method = $class->getMethod($name);
      $method->setAccessible(true);
      return $method;
    }
    // ... some other functions
}

Stworzyłem również funkcję getprotectedmethod (), aby była wyraźna, czego się oczekuje, ale to zależy od Ciebie.

Zdrówko!
 17
Author: teastburn,
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
2017-05-23 12:02:48

Myślę, że troelskn jest blisko. Zrobiłbym to zamiast:

class ClassToTest
{
   protected testThisMethod()
   {
     // Implement stuff here
   }
}

Następnie zaimplementuj coś takiego:

class TestClassToTest extends ClassToTest
{
  public testThisMethod()
  {
    return parent::testThisMethod();
  }
}

Następnie wykonujesz testy na TestClassToTest.

Powinno być możliwe Automatyczne generowanie takich klas rozszerzeń przez parsowanie kodu. Nie zdziwiłbym się, gdyby PHPUnit oferował już taki mechanizm(choć nie sprawdzałem).

 8
Author: Michael Johnson,
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
2008-10-31 18:36:25

Wrzucę tu swój kapelusz do ringu:

Użyłem _ _ call hack z mieszanym stopniem sukcesu. Alternatywą, którą wymyśliłem, było użycie wzoru odwiedzającego:

1: generowanie klasy stdClass lub niestandardowej klasy (aby wymusić typ)

2: prime że z wymaganą metodą i argumentami

3: Upewnij się, że Twój SUT ma metodę acceptVisitor, która wykona metodę z argumentami podanymi w klasie visiting

4: klasa, którą chcesz przetestować

5: SUT wstrzykuje wynik operacji do odwiedzającego

6: Zastosuj warunki testu do atrybutu wyniku odwiedzającego

 3
Author: sunwukung,
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
2010-08-26 15:59:45

Możesz rzeczywiście użyć _ _ call() w sposób ogólny, aby uzyskać dostęp do chronionych metod. Aby móc przetestować tę klasę

class Example {
    protected function getMessage() {
        return 'hello';
    }
}

Tworzysz podklasę w ExampleTest.php:

class ExampleExposed extends Example {
    public function __call($method, array $args = array()) {
        if (!method_exists($this, $method))
            throw new BadMethodCallException("method '$method' does not exist");
        return call_user_func_array(array($this, $method), $args);
    }
}

Zauważ, że metoda _ _ call() nie odwołuje się do klasy w żaden sposób, więc możesz skopiować powyższe dla każdej klasy z chronionymi metodami, które chcesz przetestować i po prostu zmienić deklarację klasy. Możesz być w stanie umieścić tę funkcję we wspólnej klasie bazowej, ale nie próbowałem.

Teraz przypadek testowy różni się tylko tym, gdzie konstruuje się obiekt, który ma być testowany, np. w ExampleExposed.

class ExampleTest extends PHPUnit_Framework_TestCase {
    function testGetMessage() {
        $fixture = new ExampleExposed();
        self::assertEquals('hello', $fixture->getMessage());
    }
}

Wierzę, że PHP 5.3 pozwala na użycie reflection do zmiany dostępności metod bezpośrednio, ale zakładam, że będziesz musiał to zrobić dla każdej metody indywidualnie.

 3
Author: David Harkness,
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-06-23 17:14:55

Proponuję następujące obejście obejścia / pomysłu "Henrik Paul":)

Znasz nazwy prywatnych metod swojej klasy. Na przykład są to _add(), _edit (), _delete () itd.

Dlatego, gdy chcesz przetestować ją z aspektu unit-testing, po prostu wywołaj prywatne metody przez prefiks i/lub przyrostek jakiegoś wspólnego słowa (na przykład _addPhpunit) tak, że gdy wywołana jest metoda _ _ call () (ponieważ metoda _addPhpunit () nie istnieje) klasy owner, wystarczy umieścić niezbędne kod w metodzie _ _ call (), aby usunąć prefiks/przyrostek słowa/s (Phpunit), a następnie wywołać stamtąd tę prywatną metodę. To kolejne dobre wykorzystanie magicznych metod.

Spróbuj.

 2
Author: Anirudh Zala,
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
2009-09-09 10:32:34