2010-05-05 18 views
16

Básicamente he estado programando por un tiempo y después de terminar mi último proyecto puedo entender completamente cuánto más fácil hubiera sido si hubiera hecho TDD. Supongo que todavía no lo hago estrictamente, ya que todavía estoy escribiendo código y luego estoy escribiendo una prueba, no entiendo cómo se hace la prueba antes del código si no sabes qué estructuras y cómo almacenar tus datos, etc. ... pero de todos modos ...Pruebas unitarias: ¿Lo estoy haciendo bien?

Un poco difícil de explicar, pero básicamente digamos, por ejemplo, tengo un objeto de fruta con propiedades como id, color y costo. (Todo el archivo de texto almacenado en ignoran completamente cualquier lógica de la base de datos, etc.)

FruitID FruitName FruitColor FruitCost 
    1   Apple  Red   1.2 
    2   Apple  Green  1.4 
    3   Apple  HalfHalf 1.5 

Todo esto es sólo por ejemplo. Pero digamos que tengo esto es una colección de objetos Fruit (es un List<Fruit>) en esta estructura. Y mi lógica indicará reordenar los frutos de la colección si se elimina una fruta (así es como debe ser la solución).

E.g. si se elimina 1, el objeto 2 toma la identificación de fruta 1, el objeto 3 toma la fruta id2.

Ahora quiero probar el código que he escrito, que hace el reordenamiento, etc

¿Cómo puedo configurar esto para hacer la prueba?


Aquí es donde he llegado hasta ahora. Básicamente tengo la clase fruitManager con todos los métodos, como deletefruit, etc. Tiene la lista generalmente pero he cambiado el método para probarlo para que acepte una lista, y la información sobre la fruta para eliminar, luego devuelve la lista.

Pruebas unitarias sabio: ¿Básicamente estoy haciendo esto de la manera correcta, o tengo una idea equivocada? y luego pruebo eliminar diferentes objetos/conjuntos de datos valorados para garantizar que el método esté funcionando correctamente.


[Test] 
public void DeleteFruit() 
{ 
    var fruitList = CreateFruitList(); 
    var fm = new FruitManager(); 

    var resultList = fm.DeleteFruitTest("Apple", 2, fruitList); 

    //Assert that fruitobject with x properties is not in list ? how 
} 

private static List<Fruit> CreateFruitList() 
{ 
    //Build test data 
    var f01 = new Fruit {Name = "Apple",Id = 1, etc...}; 
    var f02 = new Fruit {Name = "Apple",Id = 2, etc...}; 
    var f03 = new Fruit {Name = "Apple",Id = 3, etc...}; 

    var fruitList = new List<Fruit> {f01, f02, f03}; 
    return fruitList; 
} 
+0

No volvería a asignar identificaciones si fuera usted – UpTheCreek

+0

por el bien de esta pregunta, o diga tal vez el campo de valor se actualiza cuando se elimina una fruta, por ejemplo ... algo así – baron

+0

En CreateFruitList(), obtendría deshacerse de las variables fXX y simplemente agregar nuevas Frutas directamente a la lista ('fruitList.add (new Fruit (...))'). Sólo una pequeña objeción. –

Respuesta

12

Si no ve con qué prueba debe comenzar, es probable que no haya pensado en qué debería hacer su funcionalidad en términos simples. Trate de imaginar una lista priorizada de comportamientos básicos que se esperan.

¿Qué es lo primero que esperaría de un método Delete()? Si enviara el "producto" de eliminación en 10 minutos, ¿cuál sería el comportamiento no negociable incluido? Bueno ... probablemente eso borre el elemento.

Así:

1) [Test] 
public void Fruit_Is_Removed_From_List_When_Deleted() 

Cuando la prueba está escrito, pasar por todo el bucle de TDD (ejecutar la prueba => red; escribir solo código suficiente para hacerlo pasar => verde; refactor => verde)

Lo más importante relacionado con esto es que el método no debe modificar la lista si la fruta pasada como argumento no está en la lista. Así que la próxima prueba podría ser:

cosa
2) [Test] 
public void Invalid_Fruit_Changes_Nothing_When_Deleted() 

Siguiente especificó es que los identificadores deben ser cambiados cuando se elimina una fruta:

3) [Test] 
public void Fruit_Ids_Are_Reordered_When_Fruit_Is_Deleted() 

qué poner en esa prueba? Bueno, simplemente configure un contexto básico pero representativo que demuestre que su método se comporta como se esperaba.

Por ejemplo, cree una lista de 4 frutas, elimine la primera y verifique una por una que los 3 identificadores de frutas restantes se reordenan correctamente. Eso cubriría bastante bien el escenario básico.

Posteriormente, se podría crear pruebas unitarias para casos de error o limítrofes:

4) [Test] 
public void Fruit_Ids_Arent_Reordered_When_Last_Fruit_Is_Deleted() 

5) [Test] 
[ExpectedException] 
public void Exception_Is_Thrown_When_Fruit_List_Is_Empty() 

...

7

Antes de empezar a escribir su primera prueba, se supone que tiene una idea aproximada acerca de la estructura/diseño de su aplicación, las interfaces, etc. La fase de diseño es a menudo una especie de implicados con TDD .

Supongo que para un desarrollador experimentado es algo obvio, y al leer las especificaciones de un problema, inmediatamente comienza a visualizar el diseño de la solución en su cabeza; esta puede ser la razón por la que a menudo se clasifica de tomado por sentado. Sin embargo, para un desarrollador no tan experimentado, la actividad de diseño puede necesitar ser una tarea más explícita.

De cualquier manera, después de que el primer boceto del diseño esté listo, TDD se puede usar para verificar el comportamiento y verificar la solidez/usabilidad del diseño en sí. Puede comenzar a escribir su primera prueba de unidad, luego darse cuenta de "oh, en realidad es bastante incómodo hacer esto con la interfaz que imaginé", luego vuelve y rediseña la interfaz. Es un enfoque iterativo.

Josh Bloch habla de esto en "Coders at Work" - usualmente escribe muchos casos de uso para sus interfaces incluso antes de comenzando a implementar cualquier cosa. Entonces, dibuja la interfaz y luego escribe el código que la usa en todos los diferentes escenarios en los que puede pensar. Todavía no es compilable; lo usa simplemente para hacerse una idea de si su interfaz realmente ayuda a lograr las cosas fácilmente.

+0

pero diga que tiene eso y aún no está seguro de las estructuras de datos, etc. y de cómo manejará realmente la interfaz, ¿entonces no sabe cómo escribir la prueba? – baron

+0

¿Está empezando a tener más sentido pero rediseñando la interfaz por el bien de las pruebas unitarias? parece un sacrificio un poco difícil de hacer? a menos que al hacerlo sea más probable que produzcas una mejor interfaz de todos modos ... – baron

+4

@baron, quise exactamente rediseñar la interfaz para hacerlo mejor en general. Las pruebas unitarias son un cliente específico a este respecto. Si una interfaz es difícil de usar para una prueba unitaria, (casi siempre) significa que también es difícil de usar para otros clientes. –

1

Nunca tendrá la certeza de que su unidad de prueba cubre todas las eventualidades, por lo que es más o menos su medida personal de cuánto prueba y qué exactamente. La prueba de su unidad debería al menos probar los casos fronterizos, que no está haciendo allí. ¿Qué sucede cuando intentas eliminar una Apple con una identificación no válida? ¿Qué sucede si tiene una lista vacía? ¿Qué sucede si elimina la primera/última opción? Etc.

En general, no veo mucho sentido probar un solo caso especial como lo hace anteriormente. En lugar de eso siempre trato de correr un montón de pruebas, que en su caso ejemplo sugiere un enfoque ligeramente diferente:

  • En primer lugar, escribir un método corrector. Puede hacer esto tan pronto como sepa que tendrá una lista de frutas y que en esta lista todas las frutas tendrán identificaciones sucesivas (es como probar si la lista está ordenada). No se debe escribir ningún código para eliminar, más usted puede reutilizarlo f.ex. en el código de inserción de pruebas unitarias.

  • Luego, cree un grupo de listas de pruebas diferentes (quizás aleatorias) (tamaño vacío, tamaño promedio, tamaño grande). Esto tampoco requiere un código previo para su eliminación.

  • Finalmente, ejecute eliminaciones específicas para cada una de las listas de prueba (eliminar con id no válido, eliminar id 1, eliminar la última identificación, eliminar id aleatorio) y verificar el resultado con su método de comprobación. En este punto, al menos debe conocer la interfaz para su método de eliminación, pero no es necesario que ya se haya escrito.

@Update con respecto al comentario: método El corrector es más de una comprobación de consistencia en la estructura de datos. En su ejemplo, todas las frutas en la lista tienen identificaciones sucesivas, así que eso está marcado. Si tiene una estructura DAG, es posible que desee comprobar su acidez, etc.

Comprobando si la eliminación de ID x funcionó depende de si estaba presente en la lista o si su aplicación distingue el caso de un error. eliminación debido a una identificación inválida de una exitosa (de cualquier manera no queda tal ID al final). Claramente, también quiere verificar que una ID eliminada ya no está presente en la lista (aunque eso no forma parte de lo que quise decir con el método de la herramienta de verificación, en cambio, pensé que era lo suficientemente obvio como para omitir).

+0

Al eliminar la fruta con fruitid 1, estaba intentando probar la eliminación del primer artículo. Con respecto al punto 1 y el método del comprobador, ¿cómo funciona esto exactamente para verificar cosas como las que mencionas (eliminar el ID no válido, eliminar el ID 1, la última identificación, etc. ...) – baron

1

Dado que está utilizando C#, supongo que NUnit es su marco de prueba. En ese caso, tiene un rango de declaraciones de Assert [..] a su disposición.

Con respecto a los detalles de su código: No volvería a asignar los ID, ni cambiaría la composición de los objetos restantes de Fruit de ninguna manera al manipular la lista. Si necesita la identificación para realizar un seguimiento de la posición del objeto en la lista, use .IndexOf() en su lugar.

Con TDD, me parece que escribir primero la prueba suele ser algo difícil de hacer: termino escribiendo el código primero (código o serie de hacks). Un buen truco es tomar ese "código" y usarlo como prueba. Luego escriba su código real otra vez, ligeramente diferente. De esta forma, tendrá dos códigos diferentes que lograrán lo mismo: menos posibilidades de cometer el mismo error en la producción y el código de prueba. Además, tener que encontrar una segunda solución para el mismo problema puede mostrarle debilidades en su enfoque original y conducir a un mejor código.

+0

Me gusta su mención de usar el primer código como prueba código para escribir mejor código para el segundo. Personalmente, generalmente reescribir las cosas por segunda vez siempre hace que el código sea mucho más fácil de leer y entender. Sin embargo, tal vez la reasignación de la identificación era un mal ejemplo, solo dije que el ejemplo fuera más fácil. Pero digamos, por ejemplo, borrar una manzana debe provocar un nuevo cálculo del costo de otras manzanas, así que tengo que cambiar la composición de los objetos de fruta de recuerdo cuando manipulo la lista. – baron

+1

Prefiero tener pruebas que comprueben exactamente una cosa, por lo que método que elimina un elemento de un conjunto Me gustaría tener una prueba para cuando el elemento está en el conjunto junto con otros elementos, uno para cuando no está en un conjunto no vacío, uno para cuando es el único elemento en un conjunto, y uno para cuando el conjunto está vacío. –

+0

y tu también diciendo; y así sucesivamente, como una prueba para determinar si el valor de costo recalculado correctamente bajo x parámetros, ... bajo y params, etc. – baron

1
[Test] 
public void DeleteFruit() 
{ 
    var fruitList = CreateFruitList(); 
    var fm = new FruitManager(fruitList); 

    var resultList = fm.DeleteFruit(2); 

    //Assert that fruitobject with x properties is not in list 
    Assert.IsEqual(fruitList[2], fm.Find(2)); 
} 

private static List<Fruit> CreateFruitList() 
{ 
    //Build test data 
    var f01 = new Fruit {Name = "Apple",Id = 1, etc...}; 
    var f02 = new Fruit {Name = "Apple",Id = 2, etc...}; 
    var f03 = new Fruit {Name = "Apple",Id = 3, etc...}; 

    return new List<Fruit> {f01, f02, f03}; 
} 

Usted puede tratar de alguna inyección de dependencias de la lista de frutas. El objeto administrador de fruta es una tienda crud. Entonces, si tiene una operación de eliminación, necesita una operación de recuperación.

En cuanto a la reordenación, ¿quiere que suceda automáticamente o desea una operación de resort? El automáticamente también puede ser tan pronto como se produzca una operación de eliminación o un vago solo al recuperar. Ese es un detalle de implementación. Se puede decir mucho más sobre esto. Un buen comienzo para manejar este ejemplo específico sería usar Design By Contract.

[Editar 1a]

También es posible que desee considerar por qué sus pruebas para las implementaciones específicas de fruta. FruitManager debe administrar un concepto abstracto llamado Fruit. Debe tener cuidado con los detalles prematuros de implementación, a menos que esté buscando la ruta de usar DTO, pero el problema con esto es que Fruit eventualmente podría cambiar de un objeto con getters a un objeto con comportamiento real. ¡Ahora no solo fallarán sus pruebas para Fruit, sino que FruitManager fallarán!

3

Pruebas unitarias sabias: ¿Básicamente estoy haciendo esto de la manera correcta, o tengo una idea equivocada?

Te has perdido el barco.

no bastante conseguir la forma se convierte en la prueba antes de que el código si no sé qué estructuras y cómo se está almacenando datos

Este es el punto que creo que necesita para volver a, si quieres que las ideas tengan sentido.

Primer punto: las estructuras de datos y el almacenamiento se derivan de lo que necesita el código para hacer, y no al revés. Más detalladamente, si está empezando desde cero, hay varias implementaciones de estructura/almacenamiento que puede usar; de hecho, debería poder cambiar entre ellos sin necesidad de cambiar sus pruebas.

Segundo punto: en la mayoría de los casos, consume su código con más frecuencia de la que lo produce. Lo escribe una vez, pero usted (y sus colegas) lo llaman muchas veces. Por lo tanto, la conveniencia de llamar al código debería tener una prioridad más alta de lo que sería si estuvieras escribiendo la solución puramente desde adentro hacia afuera.

Así que cuando se encuentra escribiendo una prueba y descubriendo que la implementación del cliente es fea/torpe/inadecuada, se activa una advertencia antes de que haya comenzado a implementar algo. Del mismo modo, si te encuentras escribiendo mucho código de configuración en tus pruebas, te dice que realmente no has separado bien tus preocupaciones. Cuando te encuentras diciendo "guau, esa prueba fue fácil de escribir", entonces probablemente tengas una interfaz que sea fácil de usar.

Es muy difícil llegar a esto cuando se utilizan ejemplos orientados a la implementación (como escribir una prueba para un contenedor). Lo que necesita es un problema de juguete bien delimitado, independiente de la implementación.

Para un ejemplo trivial, puede considerar un administrador de autenticación: pase un identificador y un secreto, y descubra si el secreto coincide con el identificador. Por lo tanto, debe poder escribir tres pruebas rápidas desde la parte superior: verifique que el secreto correcto permita el acceso, verifique que un secreto incorrecto prohíbe el acceso, verifique que cuando se cambie un secreto, solo la nueva versión permita el acceso.

Así que quizás escriba algunas pruebas simples con nombres de usuario y contraseñas. Y a medida que lo hace, se da cuenta de que los secretos no deben limitarse a cadenas, sino que debe ser capaz de hacer un secreto de cualquier elemento serializable, y que tal vez el acceso no sea universal, sino restringido (¿eso concierne al administrador de autenticación? ? tal vez no) y oh querrás demostrar que los secretos se guardan con seguridad ...

Puedes, por supuesto, seguir este mismo enfoque para los contenedores. Pero creo que le resultará más fácil "obtenerlo" si parte de un problema de usuario/empresa, en lugar de un problema de implementación.

Pruebas unitarias que verifican una implementación específica ("¿Tenemos un error de poste de cerca aquí?") Tienen valor. El proceso para crearlos es mucho más parecido a "adivinar un error, escribir una prueba para verificar el error, reaccionar si la prueba falla". Sin embargo, estas pruebas tienden a no contribuir a su diseño: es mucho más probable que esté clonando un bloque de código y cambiando algunas entradas. Sin embargo, a menudo sucede que cuando las pruebas unitarias siguen a la implementación, a menudo son difíciles de escribir y tienen grandes costos de inicio ("¿por qué necesito cargar tres bibliotecas e iniciar un servidor web remoto para probar un error de fencepost en mi bucle for ? ").

Lectura recomendada Freeman/Pryce, creciente de software orientado a objetos, guiada por las pruebas

1

comenzar con el interfaz, tener una aplicación concreta esqueleto.Para cada método/propiedad/evento/constructor, existe un comportamiento esperado. Comience con una especificación para el primer comportamiento, y completarlo:

[Especificación] es igual que [TestFixture] [Esto] es el mismo que [Test]

[Specification] 
When_fruit_manager_has_delete_called_with_existing_fruit : FruitManagerSpecifcation 
{ 
    private IEnumerable<IFruit> _fruits; 

    [It] 
    public void Should_remove_the_expected_fruit() 
    { 
    Assert.Inconclusive("Please implement"); 
    } 

    [It] 
    public void Should_not_remove_any_other_fruit() 
    { 
    Assert.Inconclusive("Please implement"); 
    } 

    [It] 
    public void Should_reorder_the_ids_of_the_remaining_fruit() 
    { 
    Assert.Inconclusive("Please implement"); 
    } 

    /// <summary> 
    /// Setup the SUT before creation 
    /// </summary> 
    public override void GivenThat() 
    { 
    _fruits = new List<IFruit>(); 

    3.Times(_fruits.Add(Mock<IFruit>())); 

    this._fruitToDelete = _fruits[1]; 

    // this fruit is injected in th Sut 
    Dep<IEnumerable<IFruit>>() 
       .Stub(f => ((IEnumerable)f).GetEnumerator()) 
       .Return(this.Fruits.GetEnumerator()) 
       .WhenCalled(mi => mi.ReturnValue = this._fruits.GetEnumerator()); 

    } 

    /// <summary> 
    /// Delete a fruit 
    /// </summary> 
    public override void WhenIRun() 
    { 
    Sut.Delete(this._fruitToDelete); 
    } 
} 

la especificación anterior es sólo ad hoc y INCOMPLETO, pero este es un buen comportamiento TDD forma de acercarse a cada unidad/especificación.

aquí sería parte de la IVU sin aplicarse cuando empiece a trabajar en él:

public interface IFruitManager 
{ 
    IEnumerable<IFruit> Fruits { get; } 

    void Delete(IFruit); 
} 

public class FruitManager : IFruitManager 
{ 
    public FruitManager(IEnumerable<IFruit> fruits) 
    { 
    //not implemented 
    } 

    public IEnumerable<IFruit> Fruits { get; private set; } 

    public void Delete(IFruit fruit) 
    { 
    // not implemented 
    } 
} 

Así como se puede ver ningún código real está escrito. Si desea completar esa primera especificación "Cuando _...", primero tiene que hacer una [ConstructorSpecification] When_fruit_manager_is_injected_with_fruit() porque las frutas inyectadas no se asignan a la propiedad Fruits.

Así que voil, no es necesario implementar ningún código REAL al principio ... lo único que se necesita ahora es disciplina.

Una cosa que me encanta de esto es que si necesita clases adicionales durante la implementación del SUT actual, no tiene que implementarlas antes de implementar FruitManager porque puede usar simulaciones como, por ejemplo, ISomeDependencyNeeded ... y cuando completes el administrador de Fruit, entonces puedes ir y trabajar en la clase SomeDependencyNeeded. Bastante malvado

Cuestiones relacionadas