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.