vs2015 visual unitarias unit test studio pruebas proyecto mvc hacer ejecutar con c# unit-testing collections tdd

unitarias - testing in c# visual studio



Pruebas unitarias: ¿Lo estoy haciendo bien? (7)

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

Te has perdido el bote.

No entiendo cómo se hace la prueba antes del código si no sabes qué estructuras y cómo estás almacenando datos

Este es el punto al que creo que debes volver, si quieres que las ideas tengan sentido.

Primer punto: las estructuras de datos y el almacenamiento derivan de lo que necesita que haga el código, 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.

Por lo tanto, cuando se encuentre escribiendo una prueba y descubriendo que la implementación del cliente es fea / torpe / inadecuada, se activará 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, podría considerar un administrador de autenticación: pasar un identificador y un secreto, y averiguar 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.

Entonces 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 de forma segura ...

Por supuesto, puede tomar 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.

Las pruebas unitarias que verifican una implementación específica ("¿Tenemos aquí un error de publicación de cercado?") 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 software orientado a objetos, guiado por pruebas

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...

Es 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 almacenado en archivo de texto ignora completamente cualquier lógica de base de datos, etc.)

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

Esto es solo por ejemplo. Pero digamos que tengo esta es una colección de objetos de Fruit (es una 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).

Por ejemplo, si se elimina 1, el objeto 2 toma el ID de fruta 1, el objeto 3 toma el id2 de fruta.

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

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

Aquí es donde he llegado tan lejos. 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 sabias: ¿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; }


Antes de que realmente comiences a escribir tu primera prueba, se supone que tienes una idea aproximada sobre la estructura / diseño de tu aplicación, las interfaces, etc. La fase de diseño a menudo está implícita con TDD.

Supongo que para un desarrollador experimentado es algo obvio, y al leer las especificaciones del 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 da por sentada. . 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 de diseño esté listo, TDD se puede usar tanto para verificar el comportamiento como para 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", que generalmente escribe muchos casos de uso para sus interfaces incluso antes de comenzar a implementar algo. Entonces, dibuja la interfaz y luego escribe el código que la usa en todos los diferentes escenarios en los que puede pensar. Aún no es compilable; lo usa simplemente para hacerse una idea de si su interfaz realmente ayuda a lograr las cosas fácilmente.


Comience con la interfaz, tenga una implementación esquemática concreta. Para cada método / propiedad / evento / constructor, existe un comportamiento esperado. Comience con una especificación para el primer comportamiento y complétela:

[Especificación] es lo mismo que [TestFixture] [It] es lo 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 solo adhoc e INCOMPLETA, pero esta es una buena forma de abordar TDD de cada unidad / especificación.

Aquí sería parte del SUT no implementado cuando empiezas 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 } }

Entonces, como pueden ver, no se escribe un código real. 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, voila, 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 completa Fruit administrador, entonces puede ir y trabajar en la clase SomeDependencyNeeded. Bastante malvado


Como 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 un poco 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 de nuevo , de forma 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.


Nunca estará seguro de que su prueba unitaria cubra todas las eventualidades, por lo que es más o menos su medida personal en cuanto a la cantidad de pruebas y también 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 el primer / último elemento, etc.

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

  • Primero, escribe un método de verificación. 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 la identificación aleatoria) y verificar el resultado con su método de corrector. 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: el método checker es más una verificación de coherencia 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.

Probar si la eliminación de ID x funcionó depende de si estaba presente en la lista y si su aplicación distingue el caso de una eliminación fallida debido a una ID no válida de una exitosa (de cualquier manera no queda tal ID en el fin). 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).


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ías 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.

Asi que :

1) [Test] public void Fruit_Is_Removed_From_List_When_Deleted()

Cuando se escriba esa prueba, pase por todo el ciclo TDD (ejecute test => red; escriba el código suficiente para que pase => verde; refactor => verde)

Lo siguiente 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. Entonces la próxima prueba podría ser:

2) [Test] public void Invalid_Fruit_Changes_Nothing_When_Deleted()

Lo siguiente que especificó es que los identificadores deben reorganizarse 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.

Luego podría crear pruebas unitarias para casos de error o límite:

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

...


[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}; }

Puede probar alguna inyección de dependencia 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, ¿quieres que suceda automáticamente o quieres 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é su prueba para implementaciones específicas de Fruit . FruitManager debería 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 de Fruit , sino que FruitManager fallará!