Best practice per testare metodi protetti con PHPUnit


287

Ho trovato la discussione su Testare il metodo privato informativo.

Ho deciso che in alcune classi voglio avere metodi protetti, ma testarli. Alcuni di questi metodi sono statici e brevi. Poiché la maggior parte dei metodi pubblici li utilizza, probabilmente sarò in grado di rimuovere in sicurezza i test in seguito. Ma per iniziare con un approccio TDD ed evitare il debug, voglio davvero testarli.

Ho pensato a quanto segue:

  • Metodo Oggetto come consigliato in una risposta sembra essere eccessivo per questo.
  • Inizia con metodi pubblici e quando la copertura del codice viene fornita da test di livello superiore, rendili protetti e rimuovi i test.
  • Eredita una classe con un'interfaccia testabile che rende pubblici i metodi protetti

Qual è la migliore pratica? C'è niente altro?

Sembra che JUnit cambi automaticamente i metodi protetti per renderli pubblici, ma non ho avuto uno sguardo più approfondito. PHP non lo consente tramite la riflessione .


Due domande: 1. perché dovresti preoccuparti della funzionalità di test che la tua classe non espone? 2. Se dovresti testarlo, perché è privato?
nad2000,

2
Forse vuole verificare se una proprietà privata è stata impostata correttamente e l'unico modo per testare utilizzando solo la funzione setter è rendere pubblica la proprietà privata e controllare i dati
AntonioCS

4
E quindi questo è in stile discussione e quindi non costruttivo. Ancora :)
mlvljr

72
Puoi chiamarlo contro le regole del sito, ma chiamarlo "non costruttivo" è ... è offensivo.
Andy V,

1
@Visser, si sta insultando;)
Pacerier

Risposte:


417

Se stai usando PHP5 (> = 5.3.2) con PHPUnit, puoi testare i tuoi metodi privati ​​e protetti usando reflection per impostarli come pubblici prima di eseguire i test:

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

27
Per citare il link al blog dei sebastians: "Quindi: solo perché è possibile testare attributi e metodi protetti e privati ​​non significa che questa sia una" cosa buona "." - Solo per tenerlo a mente
Edorian l'

10
Lo contesterei. Se non hai bisogno dei tuoi metodi protetti o privati ​​per funzionare, non provarli.
Uckelman,

10
Giusto per chiarire, non è necessario utilizzare PHPUnit perché questo funzioni. Funzionerà anche con SimpleTest o altro. Non c'è nulla nella risposta che dipende da PHPUnit.
Ian Dunn,

84
Non devi testare direttamente i membri protetti / privati. Appartengono all'implementazione interna della classe e non devono essere accoppiati al test. Ciò rende impossibile il refactoring e alla fine non si verifica ciò che deve essere testato. È necessario testarli indirettamente utilizzando metodi pubblici. Se lo trovi difficile, quasi sicuro che ci sia un problema con la composizione della classe e devi separarlo in classi più piccole. Tieni presente che la tua classe dovrebbe essere una scatola nera per il tuo test: aggiungi qualcosa e ottieni qualcosa in cambio, e questo è tutto!
gphilip,

24
@gphilip Per me, il protectedmetodo fa anche parte dell'API pubblico perché qualsiasi classe di terze parti può estenderlo e usarlo senza alcuna magia. Quindi penso che solo i privatemetodi rientrino nella categoria dei metodi da non testare direttamente. protectede publicdovrebbe essere testato direttamente.
Filip Halaxa,

48

Sembri già consapevole, ma lo riaffermerò comunque; È un brutto segno, se è necessario testare metodi protetti. Lo scopo di un unit test è testare l'interfaccia di una classe e i metodi protetti sono dettagli di implementazione. Detto questo, ci sono casi in cui ha senso. Se usi l'ereditarietà, puoi vedere una superclasse che fornisce un'interfaccia per la sottoclasse. Quindi qui, dovresti testare il metodo protetto (ma mai uno privato ). La soluzione a questo è creare una sottoclasse a scopo di test e utilizzarla per esporre i metodi. Per esempio.:

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

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

Nota che puoi sempre sostituire l'eredità con la composizione. Quando si esegue il test del codice, in genere è molto più semplice gestire il codice che utilizza questo modello, quindi è consigliabile prendere in considerazione tale opzione.


2
Puoi semplicemente implementare direttamente stuff () come pubblico e restituire parent :: stuff (). Vedi la mia risposta Oggi sto leggendo le cose troppo velocemente.
Michael Johnson,

Hai ragione; È valido per cambiare un metodo protetto in uno pubblico.
troelskn,

Quindi il codice suggerisce la mia terza opzione e "Nota che puoi sempre sostituire l'eredità con la composizione". va nella direzione della mia prima opzione o refactoring.com/catalog/replaceInheritanceWithDelegation.html
GrGr,

34
Non sono d'accordo che sia un brutto segno. Facciamo la differenza tra TDD e Unit Testing. I test unitari dovrebbero testare i metodi privati ​​imo, dal momento che si tratta di unità e trarrebbero beneficio proprio come i metodi pubblici di testing unitari traggono vantaggio dai test unitari.
Koen,

36
I metodi protetti fanno parte dell'interfaccia di una classe, non sono semplicemente dettagli di implementazione. L'intero punto dei membri protetti è che le sottoclassi (utenti a sé stanti) possono usare quei metodi protetti all'interno delle uscite di classe. Quelli chiaramente devono essere testati.
BT,

40

teastburn ha l'approccio giusto. Ancora più semplice è chiamare direttamente il metodo e restituire la risposta:

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

Puoi chiamarlo semplicemente nei tuoi test:

$returnVal = PHPUnitUtil::callMethod(
                $this->object,
                '_nameOfProtectedMethod', 
                array($arg1, $arg2)
             );

1
Questo è un ottimo esempio, grazie. Il metodo dovrebbe essere pubblico anziché protetto, no?
Valk,

Buon punto. In realtà utilizzo questo metodo nella mia classe base da cui estendo le mie classi di test, nel qual caso ha senso. Il nome della classe sarebbe sbagliato qui.
robert.egginton,

Ho realizzato lo stesso identico codice basandomi su teastburn xD
Nebulosar il

23

Vorrei proporre una leggera variazione per getMethod () definito nella risposta di Uckelman .

Questa versione modifica getMethod () rimuovendo i valori codificati e semplificando un po 'l'utilizzo. Ti consiglio di aggiungerlo alla tua classe PHPUnitUtil come nell'esempio sotto o alla tua classe di estensione PHPUnit_Framework_TestCase (o, suppongo, a livello globale al tuo file PHPUnitUtil).

Poiché MyClass viene comunque istanziato e ReflectionClass può accettare una stringa o un oggetto ...

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
}

Ho anche creato una funzione alias getProtectedMethod () per essere esplicito su cosa ci si aspetta, ma dipende da te.

Saluti!


+1 per l'utilizzo dell'API della classe di riflessione.
Bill Ortell,

10

Penso che troelskn sia vicino. Vorrei fare questo invece:

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

Quindi, implementa qualcosa del genere:

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

Quindi eseguire i test con TestClassToTest.

Dovrebbe essere possibile generare automaticamente tali classi di estensione analizzando il codice. Non sarei sorpreso se PHPUnit offre già un tale meccanismo (anche se non ho controllato).


Eh ... sembra che lo stia dicendo, usa la tua terza opzione :)
Michael Johnson,

2
Sì, questa è esattamente la mia terza opzione. Sono abbastanza sicuro che PHPUnit non offre un tale meccanismo.
GrGr,

Questo non funzionerà, non è possibile ignorare una funzione protetta con una funzione pubblica con lo stesso nome.
Koen.

Potrei sbagliarmi, ma non penso che questo approccio possa funzionare. PHPUnit (per quanto io l'abbia mai usato) richiede che la tua classe di test estenda un'altra classe che fornisca l'effettiva funzionalità di test. A meno che non ci sia un modo per aggirare il fatto che non sono sicuro di poter vedere come questa risposta può essere utilizzata. phpunit.de/manual/current/en/…
Cypher

FYI questo funziona solo per metodi protetti , non per quelli privati
Sliq

5

Ho intenzione di gettare il mio cappello sul ring qui:

Ho usato l'hack __call con diversi gradi di successo. L'alternativa che mi è venuta in mente è stata quella di utilizzare il modello Visitatore:

1: genera una classe stdClass o personalizzata (per imporre il tipo)

2: adescalo con il metodo e gli argomenti richiesti

3: assicurarsi che il SUT disponga di un metodo acceptVisitor che eseguirà il metodo con gli argomenti specificati nella classe ospite

4: iniettalo nella classe che desideri testare

5: SUT inietta il risultato dell'operazione nel visitatore

6: applica le condizioni del test all'attributo dei risultati del visitatore


1
+1 per una soluzione interessante
jsh

5

Puoi davvero usare __call () in modo generico per accedere a metodi protetti. Per essere in grado di testare questa classe

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

crei una sottoclasse in 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);
    }
}

Si noti che il metodo __call () non fa riferimento alla classe in alcun modo, quindi è possibile copiare quanto sopra per ogni classe con metodi protetti che si desidera testare e modificare semplicemente la dichiarazione di classe. Potresti riuscire a posizionare questa funzione in una classe base comune, ma non l'ho ancora provata.

Ora lo stesso test case differisce solo da dove costruisci l'oggetto da testare, scambiando in ExampleExposed for Example.

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

Credo che PHP 5.3 ti consenta di usare la riflessione per cambiare direttamente l'accessibilità dei metodi, ma suppongo che dovresti farlo per ogni metodo individualmente.


1
L'implementazione __call () funziona alla grande! Ho provato a votare, ma ho annullato il voto fino a dopo aver testato questo metodo e ora non mi è permesso votare a causa di un limite di tempo in SO.
Adam Franco,

La call_user_method_array()funzione è obsoleta a partire da PHP 4.1.0 ... usa call_user_func_array(array($this, $method), $args)invece. Nota che se stai usando PHP 5.3.2+ puoi usare Reflection per accedere a metodi e attributi protetti / privati
nuqqsa

@nuqqsa - Grazie, ho aggiornato la mia risposta. Da allora ho scritto un Accessiblepacchetto generico che utilizza la riflessione per consentire ai test di accedere a proprietà e metodi privati ​​/ protetti di classi e oggetti.
David Harkness,

Questo codice non funziona per me su PHP 5.2.7 - il metodo __call non viene invocato per i metodi definiti dalla classe base. Non riesco a trovarlo documentato, ma suppongo che questo comportamento sia stato modificato in PHP 5.3 (dove ho confermato che funziona).
Russell Davis,

@Russell: __call()viene invocato solo se il chiamante non ha accesso al metodo. Poiché la classe e le sue sottoclassi hanno accesso ai metodi protetti, le chiamate a loro non verranno eseguite __call(). Puoi pubblicare il tuo codice che non funziona in 5.2.7 in una nuova domanda? Ho usato quanto sopra in 5.2 e sono passato all'utilizzo di reflection con 5.3.2.
David Harkness,

2

Suggerisco di seguire la soluzione alternativa per la soluzione / idea di "Henrik Paul" :)

Conosci i nomi dei metodi privati ​​della tua classe. Ad esempio sono come _add (), _edit (), _delete () ecc.

Quindi, quando si desidera testarlo dall'aspetto del test unitario, è sufficiente chiamare metodi privati ​​prefissando e / o suffissando alcuni comuni parola (ad esempio _addPhpunit) in modo che quando viene chiamato il metodo __call () (poiché il metodo _addPhpunit () non lo fa esiste) della classe del proprietario, basta inserire il codice necessario nel metodo __call () per rimuovere le parole / i prefissi / suffissi (Phpunit) e quindi chiamare quel metodo privato dedotto da lì. Questo è un altro buon uso dei metodi magici.

Provalo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.