Domanda

sto mettendo insieme una presentazione sui vantaggi di Unit Testing e vorrei un semplice esempio delle conseguenze non intenzionali: la modifica del codice in una classe che la funzionalità si rompe in un'altra classe.

Qualcuno può suggerire un semplice, facile da spiegare un esempio di questo?

Il mio piano è quello di unit test di scrittura intorno a questa funzionalità per dimostrare che sappiamo abbiamo rotto qualcosa da subito l'esecuzione del test.

È stato utile?

Soluzione

A leggermente più semplice, e quindi forse più chiara, esempio:

public string GetServerAddress()
{
    return "172.0.0.1";
}

public void DoSomethingWithServer()
{
    Console.WriteLine("Server address is: " +  GetServerAddress());
}

Se le modifiche GetServerAddress è quello di restituire un array:

public string[] GetServerAddress()
{
    return new string[] { "127.0.0.1", "localhost" };
}

L'output di DoSomethingWithServer sarà un po 'diversa, ma sarà tutto ancora compilare, rendendo per un bug ancora più sottile.

La prima versione (non-array) stamperà Server address is: 127.0.0.1 e il secondo stamperà Server address is: System.String[], questo è qualcosa che ho anche visto nel codice di produzione. Inutile dire che non c'è più!

Altri suggerimenti

Ecco un esempio:

class DataProvider {
    public static IEnumerable<Something> GetData() {
        return new Something[] { ... };
    }
}

class Consumer {
    void DoSomething() {
        Something[] data = (Something[])DataProvider.GetData();
    }
}

Cambia GetData() per restituire un List<Something>, e Consumer si romperà.

Questa potrebbe vedere un po 'forzato, ma ho visto problemi simili nel codice reale.

Diciamo che avete un metodo che fa:

abstract class ProviderBase<T>
{
  public IEnumerable<T> Results
  {
    get
    {
      List<T> list = new List<T>();
      using(IDataReader rdr = GetReader())
        while(rdr.Read())
          list.Add(Build(rdr));
      return list;
    }
  }
  protected abstract IDataReader GetReader();
  protected T Build(IDataReader rdr);
}

Con diverse implementazioni in uso. Uno di loro viene utilizzato in:

public bool CheckNames(NameProvider source)
{
  IEnumerable<string> names = source.Results;
  switch(names.Count())
  {
      case 0:
        return true;//obviously none invalid.
      case 1:
        //having one name to check is a common case and for some reason
        //allows us some optimal approach compared to checking many.
        return FastCheck(names.Single());
      default:
        return NormalCheck(names)
  }
}

Ora, niente di tutto questo è particolarmente strano. Non stiamo assumendo una particolare implementaiton di IEnumerable. In effetti, questo lavoro per gli array e molto numerose collezioni comunemente usati (non può pensare di uno in System.Collections.Generic che non corrisponde la parte superiore della mia testa). Abbiamo usato solo i metodi normali, e le normali metodi di estensione. Non è nemmeno raro avere un caso ottimizzato per le collezioni singolo elemento. Potremmo per esempio il cambiamento della lista ad essere una matrice, o forse un HashSet (per rimuovere automaticamente i duplicati), o un LinkedList o un paio di altre cose e si metterà a continuare a lavorare.

Still, mentre non siamo in funzione di un particolare implementazione, stiamo dipende una particolare funzione, in particolare quella di essere riavvolgibile (Count() sarà o chiamare ICollection.Count oppure enumerare l'enumerable, dopo di che il controllo dei nomi avrà luogo.

Qualcuno però vede immobili Risultati e pensa "hmm, che è un po 'uno spreco". Essi sostituiscono con:

public IEnumerable<T> Results
{
  get
  {
    using(IDataReader rdr = GetReader())
      while(rdr.Read())
        yield return Build(rdr);
  }
}

Questa è ancora una volta perfettamente ragionevole, e sarà davvero portare ad un notevole incremento delle prestazioni in molti casi. Se CheckNames non viene colpito nelle immediate "prove" fatte dal codificatore in questione (forse non è colpito in un sacco di percorsi di codice), allora il fatto che CheckNames saranno errore (e possibilmente restituire un risultato falso nel caso di più di 1 nome, che può essere anche peggio, se si apre un rischio per la sicurezza).

Qualsiasi prova di unità che visite per CheckNames con i risultati più di zero sta per prenderlo però.


Per inciso comparabili (se più complicata) cambiamento è una ragione per una caratteristica retrocompatibilità in NPGSQL. Non altrettanto semplice come sostituzione di un List.Add () con una resa di ritorno, ma un cambiamento del modo ExecuteReader lavorato ha dato un cambiamento comparabile da O (n) a O (1) per ottenere il primo risultato. Tuttavia, prima di allora NpgsqlConnection ha permesso agli utenti di ottenere un altro lettore da una connessione, mentre il primo era ancora aperto, e dopo che non ha fatto. La documentazione per IDbConnection dice che non si dovrebbe fare questo, ma questo non significava che non c'era nessun codice in esecuzione che ha fatto. Per fortuna un tale pezzo di eseguire codice era un test NUnit, e una funzione di retro-compatibilità aggiunto per consentire tale codice di continuare a funzionare con solo un cambiamento alla configurazione.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top