Devo testare i metodi ereditati?


30

Supponiamo di avere un manager di classe derivato da un dipendente di classe base e che il dipendente abbia un metodo getEmail () ereditato da Manager . Devo verificare che il comportamento del metodo getEmail () di un manager è in realtà lo stesso di quello di un dipendente?

Al momento della stesura di questi test, il comportamento sarà lo stesso, ma ovviamente ad un certo punto in futuro qualcuno potrebbe ignorare questo metodo, modificarne il comportamento e quindi interrompere la mia applicazione. Tuttavia sembra un po 'strano testare essenzialmente l' assenza di codice ingerente.

(Si noti che il test del metodo Manager :: getEmail () non migliora la copertura del codice (o in effetti qualsiasi altra metrica della qualità del codice (?)) Fino a quando Manager :: getEmail () non viene creato / ignorato.)

(Se la risposta è "Sì", potrebbero essere utili alcune informazioni su come gestire i test condivisi tra le classi base e derivate.)

Una formulazione equivalente della domanda:

Se una classe derivata eredita un metodo da una classe base, come si esprime (si verifica) se si prevede che il metodo ereditato:

  1. Comportarsi esattamente allo stesso modo della base in questo momento (se il comportamento della base cambia, il comportamento del metodo derivato no);
  2. Si comportano esattamente allo stesso modo della base per tutti i tempi (se cambia il comportamento della classe base, cambia anche il comportamento della classe derivata); o
  3. Comportati come vuole (non ti interessa il comportamento di questo metodo perché non lo chiami mai).

1
L'IMO derivante da una Managerclasse è Employeestato il primo grande errore.
Codici A Caos

4
@CodesInChaos Sì, potrebbe essere un cattivo esempio. Ma lo stesso problema si applica ogni volta che hai eredità.
mjs

1
Non è nemmeno necessario ignorare il metodo. Il metodo della classe base può chiamare altri metodi di istanza che sono stati sovrascritti e l'esecuzione dello stesso codice sorgente per il metodo crea comunque un comportamento diverso.
gnasher729,

Risposte:


23

Adotterò qui l'approccio pragmatico: se in futuro qualcuno sostituisce Manager :: getMail, è responsabilità dello sviluppatore fornire il codice di prova per il nuovo metodo.

Naturalmente questo è valido solo se Manager::getEmailha davvero lo stesso percorso del codice di Employee::getEmail! Anche se il metodo non viene ignorato, potrebbe comportarsi diversamente:

  • Employee::getEmailpotrebbe chiamare alcuni virtual protetti getInternalEmailche sono sovrascritti Manager.
  • Employee::getEmailpotrebbe accedere a uno stato interno (ad esempio un campo _email), che può differire nelle due implementazioni: Ad esempio, l'implementazione predefinita di Employeepotrebbe garantire che _emailsia sempre firstname.lastname@example.com, ma Managerpiù flessibile nell'assegnazione degli indirizzi di posta.

In tali casi, è possibile che un bug si manifesti solo in Manager::getEmail, anche se l'implementazione del metodo stesso è la stessa. In tal caso testare Manager::getEmailseparatamente potrebbe avere senso.


14

Vorrei.

Se stai pensando "bene, in realtà chiama solo Employee :: getEmail () perché non lo sostituisco, quindi non ho bisogno di testare Manager :: getEmail ()", non stai davvero testando il comportamento di Manager :: getEmail ().

Penserei a cosa dovrebbe fare solo Manager :: getEmail (), non se sia ereditato o ignorato. Se il comportamento di Manager :: getEmail () dovrebbe essere quello di restituire qualunque cosa restituisca Employee :: getMail (), allora questo è il test. Se il comportamento è di restituire "pink@unicorns.com", questo è il test. Non importa se è implementato per eredità o sovrascritto.

Ciò che conta è che, se cambia in futuro, il tuo test lo prende e sai che qualcosa si è rotto o deve essere riconsiderato.

Alcune persone potrebbero non essere d'accordo con l'apparente ridondanza nel controllo, ma il mio contrario sarebbe che stai testando il comportamento Employee :: getMail () e Manager :: getMail () come metodi distinti, indipendentemente dal fatto che siano ereditati o meno override. Se uno sviluppatore futuro deve modificare il comportamento di Manager :: getMail (), deve anche aggiornare i test.

Le opinioni possono tuttavia variare, penso che Digger e Heinzi abbiano fornito giustificazioni ragionevoli per il contrario.


1
Mi piace l'argomentazione secondo cui il test comportamentale è ortogonale al modo in cui la classe sembra essere costruita. Tuttavia, sembra un po 'divertente ignorare completamente l'eredità. (E come gestire i test condivisi, se hai molta eredità?)
mjs

1
Ben messo. Solo perché Manager utilizza un metodo ereditato, non significa in alcun modo che non debba essere testato. Un mio grande esempio una volta disse: "Se non viene testato, è rotto". A tal fine, se si eseguono i test per Employee and Manager richiesti al check-in del codice, si sarà sicuri che lo sviluppatore che controlla il nuovo codice per Manager che potrebbe alterare il comportamento del metodo ereditato correggerà il test in modo da riflettere il nuovo comportamento . O bussare alla tua porta.
uragano:

2
Vuoi davvero testare la classe Manager. Il fatto che sia una sottoclasse di Employee e che getMail () sia implementato basandosi sull'ereditarietà, è solo un dettaglio dell'implementazione che dovresti ignorare quando crei unit test. Il mese prossimo scopri che ereditare Manager da Employee era una cattiva idea e sostituisci l'intera struttura ereditaria e reimplementa tutti i metodi. I test unitari dovrebbero continuare a testare il codice senza problemi.
gnasher729,

5

Non provare l'unità. Fare il test funzionale / di accettazione.

I test unitari dovrebbero testare ogni implementazione, se non si fornisce una nuova implementazione, attenersi al principio DRY. Se vuoi spendere qualche sforzo qui, puoi migliorare il test unitario originale. Solo se si ignora il metodo, è necessario scrivere un test unitario.

Allo stesso tempo, i test funzionali / di accettazione dovrebbero assicurarsi che alla fine della giornata tutto il codice faccia quello che dovrebbe, e si spera coglierà qualsiasi stranezza dall'eredità.


4

Le regole di Robert Martin per TDD sono:

  1. Non è consentito scrivere alcun codice di produzione a meno che non si debba effettuare un test unit unit fallito.
  2. Non è consentito scrivere più unit test di quanto sia sufficiente per fallire; e gli errori di compilazione sono errori.
  3. Non è consentito scrivere più codice di produzione di quanto sia sufficiente per superare un test unitario non riuscito.

Se segui queste regole, non sarai in grado di porre questa domanda. Se il getEmailmetodo deve essere testato, sarebbe stato testato.


3

Sì, dovresti testare i metodi ereditati perché in futuro potrebbero essere sostituiti. Inoltre, i metodi ereditati possono chiamare metodi virtuali che vengono sovrascritti , il che cambierebbe il comportamento del metodo ereditato non sovrascritto.

Il modo in cui lo provo è creando una classe astratta per testare la classe o l'interfaccia (possibilmente astratta), come questa (in C # usando NUnit):

public abstract class EmployeeTests
{
    protected abstract Employee CreateInstance(string name, int age);

    [Test]
    public void GetEmail_ReturnsValidEmailAddress()
    {
        // Given
        var sut = CreateInstance("John Doe", 20);

        // When
        string email = sut.GetEmail();

        // Then
        Assert.IsTrue(Helper.IsValidEmail(email));
    }
}

Quindi, ho una classe con test specifici per Managere integro i test dei dipendenti in questo modo:

[TestFixture]
public class ManagerTests
{
    // Other tests.

    [TestFixture]
    public class ManagerEmployeeTests : EmployeeTests
    {
        protected override Employee CreateInstance(string name, int age);
        {
            return new Manager(name, age);
        }
    }
}

Il motivo per cui posso farlo è questo il principio di sostituzione di Liskov: i test di Employeedovrebbero ancora passare quando viene passato un Manageroggetto, dal momento che deriva Employee. Quindi devo scrivere i miei test una sola volta e posso verificare che funzionino per tutte le possibili implementazioni dell'interfaccia o della classe base.


Buon punto sul principio di sostituzione di Liskov, hai ragione nel dire che se questo vale, la classe derivata supererà tutti i test della classe base. Tuttavia, LSP è frequentemente violato, incluso il setUp()metodo stesso di xUnit ! E praticamente ogni framework Web MVC che prevede l'override di un metodo "index" rompe anche LSP, che è fondamentalmente tutti loro.
mjs

Questa è assolutamente la risposta corretta - l'ho sentito chiamare "Abstract Test Pattern". Per curiosità, perché usare una classe nidificata anziché semplicemente mescolare i test tramite class ManagerTests : ExployeeTests? (vale a dire rispecchiare l'eredità delle classi sottoposte a test). È soprattutto estetica, per aiutare a vedere i risultati?
Luke Usherwood,

2

Devo verificare che il comportamento del metodo getEmail () di un manager sia effettivamente lo stesso di quello di un dipendente?

Direi di no dato che sarebbe un test ripetuto secondo me testerei una volta nei test dei dipendenti e sarebbe così.

Al momento della stesura di questi test il comportamento sarà lo stesso, ma ovviamente ad un certo punto in futuro qualcuno potrebbe scavalcare questo metodo, cambiarne il comportamento

Se il metodo viene ignorato, saranno necessari nuovi test per verificare il comportamento ignorato. Questo è il lavoro della persona che implementa il getEmail()metodo prioritario .


1

No, non è necessario testare i metodi ereditati. Le classi e i loro casi di test che si basano su questo metodo si interromperanno comunque se il comportamento cambia Manager.

Pensa al seguente scenario: L'indirizzo e-mail è assemblato come Nome. Cognome@esempio.com:

class Employee{
    String firstname, lastname;
    String getEmail() { 
        return firstname + "." + lastname + "@example.com";
    }
}

Hai testato l'unità e funziona bene per il tuo Employee. Hai anche creato una classe Manager:

class Manager extends Employee { /* nothing different in email generation */ }

Ora hai una classe ManagerSortche ordina i gestori in un elenco in base al loro indirizzo e-mail. Supponiamo che la generazione dell'email sia la stessa di Employee:

class ManagerSort {
    void sortManagers(Manager[] managerArrayToBeSorted)
        // sort based on email address omitted
    }
}

Scrivi un test per il tuo ManagerSort:

void testManagerSort() {
    Manager[] managers = ... // build random manager list
    ManagerSort.sortManagers(managers);

    Manager[] expected = ... // expected result
    assertEquals(expected, managers); // check the result
}

Tutto funziona bene. Ora qualcuno arriva e ignora il getEmail()metodo:

class Manager extends Employee {
    String getEmail(){
        // managers should have their lastname and firstname order changed
        return lastname + "." + firstname + "@example.com";
    }
}

Ora che succede? Il tuo testManagerSort()fallirà perché il getEmail()of è Managerstato ignorato. Esaminerai questo problema e troverai la causa. E tutto senza scrivere una testcase separata per il metodo ereditato.

Pertanto, non è necessario testare i metodi ereditati.

Altrimenti, ad esempio in Java, dovresti testare tutti i metodi ereditati da Objectlike toString(), equals()ecc. In ogni classe.


Non dovresti essere intelligente con i test unitari. Dici "Non ho bisogno di testare X perché quando X fallisce, Y fallisce". Ma i test unitari si basano sui presupposti di bug nel codice. Con i bug nel tuo codice, perché pensi che le relazioni complesse tra i test funzionino nel modo in cui ti aspetti che funzionino?
gnasher729,

@ gnasher729 Dico "Non ho bisogno di testare X nella sottoclasse perché è già stato testato nei test unitari per la superclasse ". Naturalmente, se si modifica l'implementazione di X, è necessario scrivere test appropriati per esso.
Uooo
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.