2010-08-12 14 views
8

Estoy reuniendo una presentación sobre los beneficios de Unit Testing y me gustaría un ejemplo simple de consecuencias imprevistas: Cambiar el código en una clase que rompe la funcionalidad en otra clase.Necesito un ejemplo C# de consecuencias no deseadas

¿Alguien puede sugerir un ejemplo simple y fácil de explicar?

Mi plan es escribir pruebas unitarias alrededor de esta funcionalidad para demostrar que sabemos que rompimos algo al ejecutar la prueba inmediatamente.

+0

¿Es esta pregunta independiente del idioma? – kbrimington

+0

@kbrimington C#. –

+0

C# es preferido, porque ese es el de mi audiencia; Pero puedo reescribirlo en C#, dado un buen ejemplo simple. – dgiard

Respuesta

12

A ligeramente más simple, y por lo tanto quizás más clara, el ejemplo es:

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

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

Si GetServerAddress es cambios para devolver una matriz:

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

La salida de DoSomethingWithServer será algo diferente, pero todos seguirán compilando, creando un error aún más sutil.

La primera versión (sin matriz) imprimirá Server address is: 127.0.0.1 y la segunda imprimirá Server address is: System.String[], esto es algo que también he visto en el código de producción. No hace falta decir que ya no está allí!

+0

¿Cómo demonios probarías eso? El cambio del valor de retorno se puede capturar en tiempo de compilación (por ejemplo, no se puede hacer 'String address = GetServerAddreess();'), pero capturar cadenas es casi imposible – TheLQ

+0

@TheLQ, si su código es en su lugar: 'string serverAddress = GetServerAddress(); Console.WriteLine ("La dirección del servidor es:" + serverAddress); 'obtendrás un error de compilación en este ejemplo =) Y no tiene sentido preocuparte porque el" código detallado sea menos eficiente "como si el JIT no lograra optimizarlo , Estaría muy sorprendido * Y * ¡preocupado! :-) – Rob

8

He aquí un ejemplo:

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

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

Cambio GetData() para devolver un List<Something> y Consumer se romperá.

Esto podría parecer un tanto artificial, pero he visto problemas similares en el código real.

4

Digamos que tienes un método que hace:

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 varias implementaciones que se utilice. Uno de ellos se usa en:

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

Ahora, nada de esto es particularmente extraño. No asumimos una implementación particular de IEnumerable. De hecho, esto funcionará para matrices y muchas colecciones de uso común (no se puede pensar en una en System.Collections.Generic que no coincida con la parte superior de mi cabeza). Solo hemos usado los métodos normales y los métodos de extensión normales. No es inusual tener un caso optimizado para colecciones de un solo artículo. Podríamos, por ejemplo, cambiar la lista para que sea una matriz, o tal vez un HashSet (para eliminar duplicados automáticamente), una LinkedList u otras cosas más y seguirá funcionando.

De todos modos, aunque no dependemos de una implementación en particular, dependemos de una función en particular, específicamente la de ser rebobinable (Count() llamará a ICollection.Count o enumerará a través del enumerable, después de lo cual el nombre- .. comprobación se llevará a cabo

Alguien ve aunque los resultados propiedad y piensa "hmm, eso es un poco derrochador" Ellos reemplazan con:

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

de nuevo, esto es perfectamente razonable, y de hecho va a llevar a una considerable aumento de rendimiento en muchos casos.Si CheckNames no es golpeado en las "pruebas" inmediatas hechas por el codificador en cuestión (tal vez no se encuentre en muchas rutas de código), entonces el hecho de que CheckNames generará un error (y posiblemente arrojará un resultado falso en el caso) de más de 1 nombre, que puede ser incluso peor, si abre un riesgo de seguridad).

Sin embargo, cualquier prueba de unidad que llegue a CheckNames con más de cero resultados va a atraparla.


Incidentalmente una (si es más complicado) cambio comparable es una razón para una característica de compatibilidad hacia atrás en Npgsql. No es tan simple como simplemente reemplazar un List.Add() con un rendimiento de retorno, pero un cambio en la forma en que ExecuteReader funcionó dio un cambio comparable de O (n) a O (1) para obtener el primer resultado. Sin embargo, antes de eso, NpgsqlConnection permitía a los usuarios obtener otro lector de una conexión mientras que el primero todavía estaba abierto, y luego no. Los documentos para IDbConnection dicen que no deberías hacer esto, pero eso no significaba que no hubiera código de ejecución que sí lo hiciera. Afortunadamente, una de esas partes del código de ejecución fue una prueba NUnit y una característica de compatibilidad con versiones anteriores agregada para permitir que dicho código continúe funcionando con solo un cambio en la configuración.

Cuestiones relacionadas