Rendere virtuali i metodi pubblici per facilitare la testabilità

1

Ho cercato alcune pratiche per scrivere codice verificabile e ho raccolto quanto segue:

• Metodi pubblici virtuali se non si utilizzano le interfacce - rende più semplice il mocking
• Iniezione delle dipendenze - rende più semplice il mocking • Metodi più piccoli, più mirati, di scelta: i test sono più mirati, più facili da scrivere
• Evitare le classi statiche
• Evitare singleton, tranne ove necessario
• Evita classi sigillate

Non sono sicuro di ottenere il punto del primo, rendendo i metodi pubblici virtuali, dovrei farlo davvero?

    
posta Ezoela Vacca 02.02.2018 - 08:51
fonte

2 risposte

4

Public methods virtual if not using interfaces -- makes mocking easier

Non concentrarti sui mock. I mock sono un prezioso strumento di test, ma ogni volta che crei una simulazione, stai testando il tuo codice su un ambiente artificiale, piuttosto che su quello che dovrà affrontare nella produzione. Quindi minimizza il tuo uso di mock. A tal fine, i metodi di marcatura come virtuali sono davvero uno strumento di prova di ultima istanza. Sovrascrivere un metodo per testare può spesso causare più problemi di quanti ne risolva (i test si interrompono troppo facilmente quando vengono apportate modifiche al modo in cui i due metodi interagiscono nel codice reale, ad esempio). Invece, usa le interfacce in primo luogo per quando hai bisogno di prendere in giro.

Dependency injection -- makes mocking easier

No! Smettila di fissarti su beffa. L'iniezione delle dipendenze aiuta a ridurre l'accoppiamento tra le parti del codice, rendendo la manutenzione e il test molto più semplici. Il mocking è reso più semplice (se necessario) facendo riferimento alle dipendenze iniettate tramite interfacce.

Smaller, more targeted, choesive methods -- tests are more focused, easier to write

I metodi più piccoli e più coesi hanno molti vantaggi. Tendono ad essere molto più semplici da capire per cominciare. Possono anche essere più facili da testare. Questo è sicuramente un buon consiglio.

Avoidance of static classes

Le classi statiche devono essere evitate solo quando i loro metodi hanno effetti collaterali (accesso o modifica dello stato globale, risorse esterne come il file system, database ecc.). Se i metodi sono pure , cioè si basano solo sui loro input e costanti per derivare un risultato deterministico, quindi rendili statici.

[ Addendum , come un cenno a qualcosa che ho imparato da RobertHarvey di recente, fai attenzione all'utilizzo statico di metodi che richiedono molto tempo per essere eseguiti. I test unitari dovrebbero essere veloci e potrebbe essere necessario simulare i metodi lenti, quindi non rendere tali metodi statici]

Avoid singletons, except where necessary

Evita (di fatto, non usare mai) il modello singleton. È una variabile globale glorificata. È un anti-modello. Non è mai stato necessario usarlo.

Tuttavia, dovrebbero essere utilizzate singole istanze di classi, immesse in quelle parti del codice che richiedono assolutamente l'accesso. Per favore, non finire per creare due copie di una classe solo per evitare di essere accusata di usare un singleton, per esempio.

Avoid sealed classes

Se una classe è progettata male, non usa un'interfaccia e ha metodi virtuali in modo che possano essere sostituiti dai test, quindi ciò è vero. Ma tu non vuoi quello; vuoi classi ben progettate che siano accessibili tramite interfacce e che non contengano metodi virtuali. A quel punto puoi sigillarli tutti. Lo faccio.

    
risposta data 02.02.2018 - 09:22
fonte
1

Non sono sicuro di essere d'accordo con la risposta precedente, quindi tenterò la mia. Penso che il punto che il poster precedente sta cercando di fare è che dovresti concentrarti sulla qualità del codice e sulla testabilità piuttosto che seguirli come regole dure e veloci. Quello che aggiungerei è che solo nel QA manuale, quando si scrivono test automatici (come test unitari, test di integrazione, ecc.) La qualità complessiva del codice e la strategia di test sono spesso più importanti dei punti elenco.

Quello che hai trovato è una serie di linee guida che puoi implementare nel tuo codice per renderlo più testabile. Penso che in realtà questo sia il problema all'indietro - pensa ai tuoi test, POI pensa al tuo codice. Questo è noto come Test Driven Development o TDD.

Parliamo del motivo per cui scriviamo test automatici per il nostro codice. Li scriviamo perché non vogliamo che i nostri metodi modificino il comportamento in modo imprevisto . Torna ai tuoi SOLID Principles e pensa a "O" - vogliamo che il nostro codice sia "Aperto" ad estensione ma chiuso a modificato "

Il principio Open / Closed ci incoraggia a scrivere codice che può essere esteso senza dover cambiare il codice esistente. Questo perché cambiare l'esistente di codice già in produzione comporta rischi, una strategia per mitigare questo rischio e impedire l'introduzione di bug è testare il nostro codice (manualmente o in modo automatico) e uno dei più popolari i metodi per farlo sono con Test unitari.

I test unitari suddividono il tuo codice nelle sue unità più piccole e testano il comportamento di ciascun metodo / operazione in isolamento. Se la funzionalità di ciascun metodo rimane invariata, l'applicazione più grande non cambierà in modo imprevisto e verranno introdotti meno errori.

Tuttavia, i Test unitari non sono in grado di rilevare i bug nel codice, ma aiutano solo a far rispettare questo principio Open / Closed e impediscono l'introduzione di bug nel codice già funzionante. Ecco perché i Test unitari dovrebbero far parte della tua strategia complessiva di test, non sono una pallottola d'argento.

Stabilito il valore dei Test delle unità parliamo di TDD.

Il mio problema principale con i punti elenco sopra è che implicano che stai scrivendo un codice che può essere testato in un secondo momento, un approccio molto migliore è:

  • Crea uno stub di metodo che contiene la firma e genera qualcosa come NotImplementedException
  • Crea una classe di test che rappresenti questo metodo
  • Crea test che "documentano" ciò che il metodo fa in ogni scenario, questo includerà scenari come il percorso felice, passaggio di valori nulli, passaggio di valori fuori intervallo e errori potenzialmente interni
  • Implementa il tuo metodo e assicurati che tutti i test superino

Creare un test per passare un null è abbastanza facile:

[Test]
public void An_exception_is_thrown_when_null_is_supplied()
{
  var myClass = new MyClass();
  Assert.Throws<ArgumentNullException>(() => myClass.DoSomething(null));
}

Tuttavia, testare cosa succederà se c'è un errore all'interno della classe (ad esempio accedere al database) è dove le cose diventano un po 'più complicate ed è qui che entrano mock e stub. A seguito di un'iniezione di dipendenze (o pattern DI) possiamo scrivi un test che dice:

[Test]
public void An_exception_is_thrown_when_the_database_cannot_be_contacted()
{
  var myDataAccess = new Mock<IDataAccess>();
  myDataAccess.Setup(x => ConnectToDatabase()).Thows<InvalidOperationException>());
  var myClass = new MyClass(myDataAccess.Object);

  Assert.Throws<InvalidOperationException>(() => myClass.DoSomething("user1"));
}

Ora ho chiacchierato e ho acquisito un po 'di background. Diamo un'occhiata al tuo primo punto elenco (quello che stai chiedendo).

Public methods virtual if not using interfaces -- makes mocking easier

Nella nostra implementazione possiamo ipotizzare che MyClass assomigli a qualcosa del tipo:

public class MyClass
{
  public MyClass(IDataAccess dataAccess)
  {
     // Null check dataAccess and assign to an internal variable somewhere

Tuttavia, che succede se (per qualsiasi motivo) non si può passare alla dipendenza?

Che cosa succede se l'implementazione di MyClass è simile a questa:

public class MyClass
{
  public void DoSomething(string username)
  {
    throw new NotImplementedException();
  }

  public void ConnectToDatabase()
  {
    // connect to the database

Le nostre librerie di derisione troveranno MOLTO difficile simulare un errore durante la connessione al database. Tuttavia alcune librerie di derisione ti permettono di prendere in giro metodi virtuali restituendo una classe che eredita dalla tua classe e sovrascrive i metodi virtuali. Ciò significherebbe che potresti prendere in giro il metodo ConnectToDatabase () costringendolo a generare un'eccezione per te. Tuttavia la libreria di derisione non sarà in grado di farlo se la classe è sigillata (l'ultimo punto elenco) o se i metodi non sono virtuali.

Spero che questo aiuti a spiegare perché il tuo elenco di proiettili non è l'ideale, il razionale per il primo punto elenco e come farei invece le cose?

TLDR

Rendere i metodi virtuali aiuta le librerie di derisione a sovrascriverli se DI non può essere utilizzato.

    
risposta data 02.02.2018 - 10:55
fonte

Leggi altre domande sui tag