c# - studio - pruebas unitarias interfaz
Patrones o prácticas para los métodos de pruebas unitarias que llaman a un método estático (5)
Últimamente, he estado reflexionando sobre la mejor manera de "simular" un método estático que se llama desde una clase que estoy tratando de probar. Tome el siguiente código, por ejemplo:
using (FileStream fStream = File.Create(@"C:/test.txt"))
{
string text = MyUtilities.GetFormattedText("hello world");
MyUtilities.WriteTextToFile(text, fStream);
}
Entiendo que este es un ejemplo bastante malo, pero tiene tres llamadas al método estático que son todas ligeramente diferentes. La función File.Create accede al sistema de archivos y no soy dueño de esa función. MyUtilities.GetFormattedText es una función que poseo y es puramente sin estado. Finalmente, MyUtilities.WriteTextToFile es una función que poseo y accede al sistema de archivos.
Lo que he estado reflexionando últimamente es que si esto fuera código heredado, ¿cómo podría refactorizarlo para que sea más comprobable por unidad? He escuchado varios argumentos de que las funciones estáticas no deben usarse porque son difíciles de probar. No estoy de acuerdo con esta idea porque las funciones estáticas son útiles y no creo que una herramienta útil deba descartarse solo porque el marco de prueba que se está utilizando no pueda manejarlo muy bien.
Después de mucha búsqueda y deliberación, llegué a la conclusión de que básicamente hay 4 patrones o prácticas que se pueden usar para hacer que las funciones que llaman funciones estáticas sean comprobables por unidad. Estos incluyen los siguientes:
- No se burle de la función estática y simplemente deje que la prueba de la unidad lo llame.
- Envuelva el método estático en una clase de instancia que implemente una interfaz con la función que necesita y luego use la inyección de dependencia para usarla en su clase. Me referiré a esto como inyección de dependencia de interfaz .
- Use Moles (o TypeMock) para secuestrar la llamada a la función.
- Use la inyección de dependencia para la función. Me referiré a esto como inyección de dependencia de función .
He escuchado bastantes discusiones sobre las primeras tres prácticas, pero cuando estaba pensando en soluciones para este problema, surgió la cuarta idea de la inyección de dependencia de funciones . Esto es similar a ocultar una función estática detrás de una interfaz, pero sin la necesidad de crear una interfaz y una clase contenedora. Un ejemplo de esto sería el siguiente:
public class MyInstanceClass
{
private Action<string, FileStream> writeFunction = delegate { };
public MyInstanceClass(Action<string, FileStream> functionDependency)
{
writeFunction = functionDependency;
}
public void DoSomething2()
{
using (FileStream fStream = File.Create(@"C:/test.txt"))
{
string text = MyUtilities.GetFormattedText("hello world");
writeFunction(text, fStream);
}
}
}
A veces, crear una interfaz y una clase contenedora para una llamada a función estática puede ser engorroso y puede contaminar su solución con muchas clases pequeñas cuyo único propósito es llamar a una función estática. Estoy dispuesto a escribir código que sea fácilmente comprobable, pero esta práctica parece ser una solución para un mal marco de prueba.
Al pensar en estas soluciones diferentes, llegué a un entendimiento de que todas las 4 prácticas mencionadas anteriormente se pueden aplicar en diferentes situaciones. Esto es lo que estoy pensando son las circunstancias correctas para aplicar las prácticas anteriores :
- No se burle de la función estática si es puramente sin estado y no tiene acceso a los recursos del sistema (como el sistema de archivos o una base de datos). Por supuesto, se puede argumentar que si se accede a los recursos del sistema, esto introduce estado en la función estática de todos modos.
- Use la inyección de dependencia de interfaz cuando haya varias funciones estáticas que esté utilizando y que lógicamente se puedan agregar a una única interfaz. La clave aquí es que hay varias funciones estáticas que se utilizan. Creo que en la mayoría de los casos este no será el caso. Probablemente solo se llamarán una o dos funciones estáticas en una función.
- Utilice Moles cuando esté burlando bibliotecas externas, como bibliotecas de UI o bibliotecas de bases de datos (como linq a sql). Mi opinión es que si se usa Moles (o TypeMock) para secuestrar el CLR con el fin de burlarse de su propio código, entonces esto es un indicador de que debe realizarse una refactorización para desacoplar los objetos.
- Use la inyección de dependencia de función cuando hay un número pequeño de llamadas de función estática en el código que se está probando. Este es el patrón al que me inclino en la mayoría de los casos para probar funciones que llaman funciones estáticas en mis propias clases de utilidad.
Estos son mis pensamientos, pero realmente agradecería algunos comentarios sobre esto. ¿Cuál es la mejor manera de probar el código donde se está llamando a una función estática externa?
Bienvenido a los males del estado estático.
Creo que tus directrices están bien, en general. Aquí están mis pensamientos:
La prueba unitaria de cualquier "función pura", que no produce efectos secundarios, está bien, independientemente de la visibilidad y el alcance de la función. Por lo tanto, las pruebas unitarias de métodos de extensión estáticos como "ayudantes de Linq" y el formato de cadenas en línea (como envoltorios para String.IsNullOrEmpty o String.Format) y otras funciones de utilidad sin estado son buenas.
Los Singleton son enemigos de las buenas pruebas unitarias. En lugar de implementar el patrón singleton directamente, considere registrar las clases que desea restringir a una sola instancia con un contenedor IoC e inyectándolas a las clases dependientes. Los mismos beneficios, con el beneficio adicional de que IoC puede configurarse para devolver un simulacro en sus proyectos de prueba.
Si simplemente debe implementar un singleton verdadero, considere la posibilidad de proteger el constructor predeterminado en lugar de hacerlo completamente privado, y defina un "proxy de prueba" que se deriva de su instancia singleton y permite la creación del objeto en el alcance de la instancia. Esto permite la generación de un "simulacro parcial" para cualquier método que tenga efectos secundarios.
Si su código hace referencia a estáticos incorporados (como ConfigurationManager) que no son fundamentales para el funcionamiento de la clase, extraiga las llamadas estáticas en una dependencia independiente que puede simular, o busque una solución basada en instancia. Obviamente, cualquier estática incorporada no puede probarse por unidad, pero no hay daño al usar su marco de prueba de unidad (MS, NUnit, etc.) para construir pruebas de integración, simplemente manténgalas separadas para que pueda ejecutar pruebas unitarias sin necesidad de un ambiente personalizado.
Siempre que el código haga referencia a estáticas (o tenga otros efectos secundarios) y no sea posible refactorizar en una clase completamente separada, extraiga la llamada estática en un método y pruebe todas las demás funciones de clase utilizando una "simulación parcial" de esa clase que anule el método .
La elección n. ° 1 es la mejor. No se burle, y solo use el método estático tal como existe. Esta es la ruta más simple y hace exactamente lo que necesita hacer. Ambos escenarios de "inyección" siguen llamando al método estático, por lo que no está obteniendo nada a través de todo el envoltorio adicional.
Para File.Create
y MyUtilities.WriteTextToFile
, crearía mi propio contenedor y lo inyectaría con la inyección de dependencia. Como toca el FileSystem, esta prueba podría ralentizarse debido a la E / S e incluso puede arrojar alguna excepción inesperada del FileSystem que lo lleve a pensar que su clase está equivocada, pero es ahora.
En cuanto a la función MyUtilities.GetFormattedText
, supongo que esta función solo hace algunos cambios con la cadena, nada de qué preocuparse aquí.
Solo crea una prueba unitaria para el método estático y no dudes en llamarlo dentro de los métodos para probarlo sin simularlo.
Usar la inyección de dependencia (ya sea la opción 2 o 4) es definitivamente mi método preferido para atacar esto. No solo facilita las pruebas sino que ayuda a separar preocupaciones y evitar que las clases se inflen.
Sin embargo, necesito aclarar que no es cierto que los métodos estáticos sean difíciles de probar. El problema con los métodos estáticos ocurre cuando se usan en otro método. Esto hace que el método que llama al método estático sea difícil de probar ya que no se puede burlar del método estático. El ejemplo habitual de esto es con I / O. En su ejemplo, está escribiendo texto en un archivo (WriteTextToFile). ¿Qué pasa si algo falla durante este método? Como el método es estático y no se puede burlar, no se pueden crear casos como casos de falla a demanda. Si crea una interfaz, puede simular la llamada a WriteTextToFile y hacer que simule errores. Sí, tendrá algunas interfaces y clases más, pero normalmente puede agrupar funciones similares lógicamente en una misma clase.
Sin inyección de dependencia: esta es prácticamente la opción 1, donde no se burla de nada. No veo esto como una estrategia sólida porque no permite realizar pruebas exhaustivas.
public void WriteMyFile(){
try{
using (FileStream fStream = File.Create(@"C:/test.txt")){
string text = MyUtilities.GetFormattedText("hello world");
MyUtilities.WriteTextToFile(text, fStream);
}
}
catch(Exception e){
//How do you test the code in here?
}
}
Con la Inyección de Dependencia:
public void WriteMyFile(IFileRepository aRepository){
try{
using (FileStream fStream = aRepository.Create(@"C:/test.txt")){
string text = MyUtilities.GetFormattedText("hello world");
aRepository.WriteTextToFile(text, fStream);
}
}
catch(Exception e){
//You can now mock Create or WriteTextToFile and have it throw an exception to test this code.
}
}
Por otro lado, ¿quiere que las pruebas de lógica empresarial fallen si no se puede leer / escribir en el sistema de archivos / base de datos? Si estamos probando que las matemáticas son correctas en nuestro cálculo de sueldos, no queremos que los errores de IO hagan que la prueba falle.
Sin Inyección de Dependencia:
Este es un ejemplo / método extraño pero solo lo estoy usando para ilustrar mi punto.
public int GetNewSalary(int aRaiseAmount){
//Do you really want the test of this method to fail because the database couldn''t be queried?
int oldSalary = DBUtilities.GetSalary();
return oldSalary + aRaiseAmount;
}
Con la Inyección de Dependencia:
public int GetNewSalary(IDBRepository aRepository,int aRaiseAmount){
//This call can now be mocked to always return something.
int oldSalary = aRepository.GetSalary();
return oldSalary + aRaiseAmount;
}
El aumento de la velocidad es una ventaja adicional de la burla. IO es costoso y la reducción en IO aumentará la velocidad de sus pruebas. No tener que esperar una transacción de base de datos o una función del sistema de archivos mejorará el rendimiento de las pruebas.
Nunca he usado TypeMock, así que no puedo hablar mucho sobre eso. Aunque mi impresión es la misma que la tuya, si tienes que usarla, probablemente haya alguna refactorización que se pueda hacer.