c# - unitarios - pruebas unitarias xamarin forms
¿Cómo puedo implementar pruebas unitarias en clases grandes y complejas? (6)
Estoy implementando pruebas unitarias en un sistema financiero que involucra varios cálculos. Uno de los métodos, recibe un objeto por parámetro con más de 100 propiedades, y en base a las propiedades de este objeto, calcula la devolución. Para implementar pruebas unitarias para este método, necesito tener todo este objeto lleno de valores válidos.
Entonces ... pregunta: hoy este objeto se llena a través de la base de datos. En mis Pruebas de unidad (Estoy usando NUnit), necesito evitar la base de datos y crear un objeto simulado de eso, para probar solo la devolución del método. ¿Cómo puedo probar eficientemente este método con este enorme objeto? ¿Realmente necesito llenar manualmente todas las 100 propiedades de la misma? ¿Hay alguna forma de automatizar la creación de este objeto utilizando Moq (por ejemplo)?
obs: Estoy escribiendo pruebas unitarias para un sistema que ya está creado. No es posible volver a escribir toda la arquitectura en este momento.
¡Un millón de gracias!
Utilice una base de datos en memoria para sus pruebas de unidad
Entonces ... esto técnicamente no es una respuesta, ya que dijo que las pruebas unitarias, y el uso de una base de datos en la memoria hace que sean pruebas de integración, no pruebas unitarias. Sin embargo, me parece que a veces, cuando se enfrentan con restricciones imposibles, es necesario dar en algún lugar y este puede ser uno de esos momentos.
Mi sugerencia es usar SQLite (o similar) en su prueba de unidad. Existen herramientas para extraer y duplicar su base de datos real en una base de datos SQLite, luego puede generar los scripts y cargarlos en una versión en memoria de la base de datos. Puede usar la inyección de dependencia y el patrón de repositorio para establecer el proveedor de base de datos diferente en sus pruebas de "unidad" que en el código real.
De esta manera, puede utilizar sus datos existentes, modificarlos cuando sea necesario como condiciones previas para sus pruebas. Deberá reconocer que esto no es una prueba de unidad verdadera ... lo que significa que está limitado a lo que realmente puede generar la base de datos (es decir, las restricciones de la tabla evitarán probar ciertos escenarios), por lo que no puede hacer una prueba de unidad completa en ese sentido. Además, estas pruebas se ejecutarán más lentamente porque realmente están haciendo un trabajo de base de datos, por lo que deberá planificar el tiempo adicional necesario para ejecutar estas pruebas. (Aunque todavía suelen ser bastante rápidos). Tenga en cuenta que puede bloquear cualquier otra entidad (por ejemplo, si hay una llamada de servicio además de la base de datos, es un potencial simulado).
Si este enfoque le parece útil, aquí hay algunos enlaces para comenzar.
Convertidor de SQL Server a SQLite:
https://www.codeproject.com/Articles/26932/Convert-SQL-Server-DB-to-SQLite-DB
SQLite studio: https://sqlitestudio.pl/index.rvt
(Use eso para generar sus scripts para uso en memoria)
Para usar en la memoria, haga esto:
TestConnection = new SQLiteConnection ("FullUri = file :: memory:? Cache = shared");
Tengo un script separado para la estructura de la base de datos de la carga de datos, pero eso es una preferencia personal.
Espero que ayude y buena suerte.
Dadas las restricciones (diseño de código incorrecto y deuda técnica ... bromeo) una prueba de unidad será muy incómoda de llenar manualmente. Se necesitaría una prueba de integración híbrida donde tendría que golpear una fuente de datos real (no la que está en producción).
Pociones potenciales
Haga una copia de la base de datos y rellene solo las tablas / datos necesarios para completar la clase compleja dependiente. Esperemos que el código sea lo suficientemente modular como para que el acceso a los datos pueda obtener y completar la clase compleja.
Simule el acceso a los datos y pídale que importe los datos necesarios a través de una fuente alternativa (archivo plano tal vez? Csv)
Todos los demás códigos podrían centrarse en burlarse de cualquier otra dependencia necesaria para realizar la prueba de la unidad.
Salvo que la única otra opción que queda es rellenar la clase manualmente.
En un lado, esto tiene un mal olor a código, pero eso está fuera del alcance del OP, dado que no se puede cambiar en este momento. Le sugiero que mencione esto a los responsables de la toma de decisiones.
Lo primero es lo primero: debe hacer que la adquisición de este objeto se realice a través de una interfaz si el código actualmente lo está extrayendo de la base de datos. Luego puedes simular que la interfaz devuelve lo que quieras en tus pruebas de unidad.
Si estuviera en su lugar, extraería la lógica de cálculo real y escribiría las pruebas en esa nueva clase de "calculadora". Desglosa todo lo que puedas. Si la entrada tiene 100 propiedades pero no todas son relevantes para cada cálculo, use las interfaces para dividirlas. Esto hará visible la entrada esperada, mejorando también el código.
Entonces, en su caso, si su clase es "BigClass", puede crear una interfaz que se usará en un cálculo determinado. De esta manera, no está cambiando la clase existente o la forma en que el otro código funciona con ella. La lógica de la calculadora extraída sería independiente, verificable y el código, mucho más simple.
public class BigClass : ISet1
{
public string Prop1 { get; set; }
public string Prop2 { get; set; }
public string Prop3 { get; set; }
}
public interface ISet1
{
string Prop1 { get; set; }
string Prop2 { get; set; }
}
public interface ICalculator
{
CalculationResult Calculate(ISet1 input)
}
Para los casos en que tuve que obtener una gran cantidad de datos correctos reales para las pruebas, serialicé los datos en JSON y los puse directamente en mis clases de prueba. Los datos originales se pueden tomar de su base de datos y luego ser serializados. Algo como esto:
[Test]
public void MyTest()
{
// Arrange
var data = GetData();
// Act
... test your stuff
// Assert
.. verify your results
}
public MyBigViewModel GetData()
{
return JsonConvert.DeserializeObject<MyBigViewModel>(Data);
}
public const String Data = @"
{
''SelectedOcc'': [29, 26, 27, 2, 1, 28],
''PossibleOcc'': null,
''SelectedCat'': [6, 2, 5, 7, 4, 1, 3, 8],
''PossibleCat'': null,
''ModelName'': ''c'',
''ColumnsHeader'': ''Header'',
''RowsHeader'': ''Rows''
// etc. etc.
}";
Es posible que esto no sea óptimo cuando se realizan muchas pruebas como esta, ya que lleva bastante tiempo obtener los datos en este formato. Pero esto puede proporcionarle una base de datos que puede modificar para diferentes pruebas una vez que haya terminado con la serialización.
Para obtener este JSON, tendrá que consultar por separado la base de datos para este gran objeto, serializarlo en JSON a través de JsonConvert.Serialise
y grabar esta cadena en su código fuente: este bit es relativamente fácil, pero toma algo de tiempo porque necesita hacerlo. Es manual ... solo una vez.
He utilizado esta técnica con éxito cuando tuve que probar la presentación de informes y obtener datos de la base de datos no era una preocupación para la prueba actual.
ps necesitarás el paquete Newtonsoft.Json
para usar JsonConvert.DeserializeObject
Si esos 100 valores no son relevantes y necesita solo algunos de ellos, tiene varias opciones.
Puede crear un nuevo objeto (las propiedades se inicializarán con valores predeterminados, como null
para cadenas y 0
para enteros) y asignar solo las propiedades requeridas:
var obj = new HugeObject();
obj.Foo = 42;
obj.Bar = "banana";
También puede usar alguna biblioteca como AutoFixture que asignará valores ficticios para todas las propiedades en su objeto:
var fixture = new Fixture();
var obj = fixture.Create<HugeObject>();
Puede asignar las propiedades requeridas manualmente, o puede usar el generador de dispositivos
var obj = fixture.Build<HugeObject>()
.With(o => o.Foo, 42)
.With(o => o.Bar, "banana")
.Create();
Otra biblioteca útil para el mismo propósito es NBuilder
NOTA: Si todas las propiedades son relevantes para la función que está probando y deberían tener valores específicos, entonces no hay una biblioteca que adivine los valores requeridos para su prueba. La única manera es especificando los valores de prueba manualmente. Aunque puede eliminar gran cantidad de trabajo si configura algunos valores predeterminados antes de cada prueba y simplemente cambia lo que necesita para una prueba en particular. Es decir, crea uno o más métodos de ayuda que crearán un objeto con un conjunto de valores predefinidos:
private HugeObject CreateValidInvoice()
{
return new HugeObject {
Foo = 42,
Bar = "banaba",
//...
};
}
Y luego en tu prueba simplemente anula algunos campos:
var obj = CreateValidInvoice();
obj.Bar = "apple";
// ...
Yo tomaría este enfoque:
1 - Escriba pruebas de unidad para cada combinación del objeto de parámetro de entrada de propiedad 100, aprovechando una herramienta para hacer esto por usted (por ejemplo, pex, intellitest) y asegúrese de que todos se ejecutan en verde. En este punto, consulte las pruebas unitarias como pruebas de integración en lugar de pruebas unitarias, por razones que se vuelven obvias más adelante.
2 - Refactorice las pruebas en trozos SOLIDOS de código: los métodos que no llaman a otros métodos pueden considerarse genuinamente verificables por unidad, ya que no tienen dependencias de otros códigos. Los métodos restantes son todavía solo comprobables como prueba de integración.
3 - Asegúrese de que TODAS las pruebas de integración todavía se estén ejecutando en verde.
4 - Crear nuevas pruebas unitarias para el nuevo código comprobable de la unidad.
5 - Con todo funcionando en verde, puede eliminar todas / algunas de las pruebas de integración originales superfluas, solo si usted se siente cómodo haciéndolo.
6 - Con todo funcionando en verde, puede comenzar a reducir las 100 propiedades necesarias en las pruebas unitarias a solo aquellas estrictamente necesarias para cada método individual. Esto probablemente resaltará áreas para refactorización adicional, pero de todos modos simplificará el objeto de parámetros. Eso, a su vez, hará que los futuros esfuerzos de los mantenedores de código tengan menos errores, y apostaría a que las fallas históricas para abordar el tamaño del objeto de parámetro cuando tenía 50 propiedades es la razón por la que ahora son 100. Si no se aborda el problema ahora, eso significa Llévase a 150 parámetros eventualmente, lo que permite enfrentarlo, nadie quiere.