¿Es estático universalmente "malo" para las pruebas unitarias? Si es así, ¿por qué Resharper lo recomienda? [cerrado]

85

He descubierto que solo hay 3 formas de unidad de prueba (simulacro / stub) dependencias que son estáticas en C # .NET:

Dado que dos de estos no son gratuitos y uno no ha llegado a la versión 1.0, burlarse de cosas estáticas no es demasiado fácil.

¿Eso tiene métodos estáticos y tal "maldad" (en el sentido de prueba de unidad)? Y si es así, ¿por qué resharper quiere que haga algo que pueda ser estático, estático? (Suponiendo que resharper no sea también "malvado").

Aclaración: estoy hablando del escenario en el que desea probar un método por unidad y ese método llama a un método estático en una unidad / clase diferente . Según la mayoría de las definiciones de la unidad de pruebas, si usted acaba de dejar el método bajo prueba llame al método estático en la otra unidad / clase entonces estás no la unidad de pruebas, que son la integración de pruebas. (Útil, pero no una prueba unitaria).

Vaccano
fuente
3
TheLQ: Puedes. Creo que está hablando de no poder probar métodos estáticos porque la mayor parte del tiempo toca variables estáticas. Cambiando así el estado después y entre pruebas.
26
Personalmente, creo que estás llevando la definición de "unidad" demasiado lejos. "Unidad" debe ser "la unidad más pequeña que tiene sentido probar aisladamente". Ese puede ser un método, puede ser más que eso. Si el método estático no tiene estado y está bien probado, entonces tener una segunda llamada de prueba unitaria (IMO) no es un problema.
mlk
10
"Personalmente creo que estás llevando la definición de" unidad "demasiado lejos". No, es solo que va con el uso estándar y tú estás creando tu propia definición.
77
"¿por qué resharper quiere que haga algo que pueda ser estático, estático?" Resharper no quiere que hagas nada. Simplemente le hace saber que la modificación es posible y quizás deseable a partir de un POV de análisis de código. ¡Resharper no es un reemplazo para tu propio juicio!
Adam Naylor
44
@ acidzombie24. Los métodos regulares también pueden modificar el estado estático para que sean tan "malos" como los métodos estáticos. El hecho de que también puedan modificar el estado con un ciclo de vida más corto los hace aún más peligrosos. (No estoy a favor de los métodos estáticos, pero el punto sobre la modificación del estado también es un golpe para los métodos regulares, aún más)
mike30

Respuestas:

105

Mirando las otras respuestas aquí, creo que puede haber cierta confusión entre los métodos estáticos que mantienen el estado estático o causan efectos secundarios (lo que me parece una muy mala idea) y los métodos estáticos que simplemente devuelven un valor.

Los métodos estáticos que no tienen estado y no causan efectos secundarios deben ser fácilmente comprobables en la unidad. De hecho, considero que tales métodos son una forma de programación funcional de "pobre hombre"; le das al método un objeto o valor, y devuelve un objeto o valor. Nada mas. No veo cómo dichos métodos afectarían negativamente las pruebas unitarias en absoluto.

Robert Harvey
fuente
44
El método estático es comprobable por unidad, pero ¿qué pasa con los métodos que llaman al método estático? Si las personas que llaman están en otra clase, entonces tienen una dependencia que necesita ser desacoplada para hacer una prueba unitaria.
Vaccano
22
@ Vaccano: Pero si escribe métodos estáticos que no mantienen el estado y no tienen efectos secundarios, de todos modos son más o menos funcionalmente equivalentes a un trozo.
Robert Harvey
20
Los simples tal vez. Pero los más complejos pueden comenzar a arrojar excepciones y tener salidas que no se esperan (y deberían quedar atrapadas en la prueba de la unidad para el método estático, no en la prueba de la unidad para el llamador del método estático), o al menos eso es lo que yo He sido llevado a creer en la literatura que he leído.
Vaccano
21
@ Vaccano: un método estático que tiene salidas o excepciones aleatorias tiene efectos secundarios.
Tikhon Jelvis
10
@TikhonJelvis Robert estaba hablando de resultados; y las excepciones "aleatorias" no deberían ser un efecto secundario, son esencialmente una forma de salida. El punto es que cada vez que prueba un método que llama al método estático, está envolviendo ese método y todas las permutaciones de su salida potencial, y no puede probar su método de forma aislada.
Nicole
26

Parece que estás confundiendo datos estáticos y métodos estáticos . Resharper, si no recuerdo mal, recomienda hacer que los privatemétodos dentro de una clase sean estáticos si se pueden hacer, creo que esto produce un pequeño beneficio de rendimiento. ¡ No recomienda hacer "nada que pueda ser" estático!

Los métodos estáticos no tienen nada de malo y son fáciles de probar (siempre que no cambien ningún dato estático). Por ejemplo, piense en una biblioteca de matemáticas, que es un buen candidato para una clase estática con métodos estáticos. Si tiene un método (artificial) como este:

public static long Square(int x)
{
    return x * x;
}

entonces esto es eminentemente comprobable y no tiene efectos secundarios. Simplemente verifica que cuando pasas, digamos, 20 vuelves a 400. No hay problema.

Dan Diplo
fuente
44
¿Qué sucede cuando tienes otra clase que llama a este método estático? Parece simple en este caso, pero es una dependencia que no se puede aislar, excepto con una de las tres herramientas enumeradas anteriormente. Si no lo aísla, entonces no está "probando unidades", está haciendo "pruebas de integración" (porque está probando qué tan bien se "integran" sus diferentes unidades.
Vaccano
3
No pasa nada. ¿Por qué lo haría? El framework .NET está lleno de métodos estáticos. ¿Estás diciendo que tampoco puedes usarlos en ningún método que quieras probar?
Dan Diplo
3
Bueno, si su código está en el nivel / calidad de producción de .NET Framework, entonces seguro, continúe. Pero el objetivo de las pruebas unitarias es la prueba de la unidad, aisladamente. Si también está llamando a métodos en otras unidades (ya sean estáticas o no), ahora está probando su unidad más sus dependencias. No digo que no sea una prueba útil, pero no es una "prueba unitaria" según la mayoría de las definiciones. (Porque ahora está probando su unidad bajo prueba y la unidad que tiene el método estático).
Vaccano
10
Presumiblemente, usted (u otros) ya ha probado el método estático y ha demostrado que funciona (al menos como se esperaba) antes de escribir demasiado código a su alrededor. Si algo se rompe en la parte que prueba a continuación, ahí es donde debe mirar primero, no en las cosas que ya ha probado.
cHao
66
@ Vaccano Entonces, ¿cómo prueba Microsoft .NET Framework, entonces, eh? Muchas de las clases en el Marco hacen referencia a métodos estáticos en otras clases (como System.Math) sin mencionar la abundancia de métodos estáticos de fábrica, etc. Además, nunca podría usar ningún método de extensión, etc. El hecho es que funciones simples como esta son fundamental para los lenguajes modernos. Puede probarlos de forma aislada (ya que generalmente serán deterministas) y luego usarlos en sus clases sin preocuparse. No es un problema!
Dan Diplo
18

Si la verdadera pregunta aquí es "¿Cómo pruebo este código?":

public class MyClass
{
   public void MethodToTest()
   {
       //... do something
       MyStaticClass.StaticMethod();
       //...more
   }
}

Luego, simplemente refactorice el código e inyecte como de costumbre la llamada a la clase estática de esta manera:

public class MyClass
{
   private readonly IExecutor _externalExecutor;
   public MyClass(_IExecutor executor)
   {
       _exeternalExecutor = executor;
   }

   public void MethodToTest()
   {
       //... do something
       _exetrnalExecutor.DoWork();
       //...more
   }
}

public class MyStaticClassExecutor : IExecutor
{
    public void DoWork()
    {
        MyStaticClass.StaticMethod();
    }
}
Soleado
fuente
99
Afortunadamente también tenemos preguntas con respecto a la legibilidad del código y la beso en SO también :)
gbjbaanb
¿un delegado no sería más fácil y haría lo mismo?
jk.
@jk podría ser, pero es difícil usar el contenedor IoC entonces.
Soleado
15

Las estadísticas no son necesariamente malas, pero pueden limitar sus opciones cuando se trata de pruebas unitarias con falsificaciones / simulacros / trozos.

Hay dos enfoques generales para burlarse.

El primero (tradicional, implementado por RhinoMocks, Moq, NMock2; también se utilizan simulacros y trozos manuales en este campo) se basa en las costuras de prueba y la inyección de dependencia. Supongamos que está probando un código estático y tiene dependencias. Lo que sucede a menudo en el código diseñado de esta manera es que las estadísticas crean sus propias dependencias, invirtiendo la inversión de dependencia . Pronto descubrirá que no puede inyectar interfaces simuladas en el código bajo prueba que está diseñado de esta manera.

El segundo (simulacros, implementado por TypeMock, JustMock y Moles) se basa en la API de creación de perfiles de .NET . Puede interceptar cualquiera de sus instrucciones CIL y reemplazar una porción de su código con una falsa. Esto permite que TypeMock y otros productos en este campamento se burlen de cualquier cosa: estática, clases selladas, métodos privados, cosas que no están diseñadas para ser comprobables.

Hay un debate en curso entre dos escuelas de pensamiento. Uno dice, siga los principios SÓLIDOS y el diseño para la comprobabilidad (que a menudo incluye ser fácil con las estadísticas). El otro dice, compre TypeMock y no se preocupe.

azheglov
fuente
14

Mira esto: "Los métodos estáticos son la muerte para la testabilidad" . Breve resumen del argumento:

Para la prueba unitaria, debe tomar una pequeña parte de su código, volver a cablear sus dependencias y probarlo de forma aislada. Esto es difícil con los métodos estáticos, no solo en el caso de que accedan al estado global, sino incluso si solo llaman a otros métodos estáticos.

Rafał Dowgird
fuente
32
No mantengo el estado global ni causo efectos secundarios en mis métodos estáticos, por lo que este argumento me parece irrelevante. El artículo que vinculó presenta un argumento de pendiente resbaladiza que no tiene base si los métodos estáticos se limitan a un código de procedimiento simple, actuando de manera "genérica" ​​(como las funciones matemáticas).
Robert Harvey
44
@Robert Harvey: ¿cómo prueba un método unitario que utiliza un método estático de otra clase (es decir, depende de un método estático)? Si solo dejas que lo llame, entonces no estás "prueba de unidad", estás "prueba de integración"
Vaccano
10
No solo lea el artículo del blog, lea los muchos comentarios que no están de acuerdo con él. Un blog es solo una opinión, no un hecho.
Dan Diplo
8
@ Vaccano: un método estático sin efectos secundarios o estado externo es funcionalmente equivalente a un valor. Sabiendo eso, no es diferente a la unidad que prueba un método que crea un número entero. Esta es una de las ideas clave que nos brinda la programación funcional.
Steven Evers
3
Si no funciona el sistema embebido o una aplicación cuyos errores tienen el potencial de crear incidentes internacionales, OMI, cuando la "capacidad de prueba" impulsa la arquitectura, ya lo había torcido antes de adoptar las metodologías de prueba de cada rincón en primer lugar. También estoy cansado de la versión XP de todo lo que domina cada conversación. XP no es una autoridad, es una industria. Aquí está la definición original y sensata de las pruebas unitarias: python.net/crew/tbryan/UnitTestTalk/slide2.html
Erik Reppen
5

La verdad simple que rara vez se reconoce es que si una clase contiene una dependencia visible del compilador en otra clase, no se puede probar aisladamente de esa clase. Puede simular algo que parece una prueba y aparecerá en un informe como si fuera una prueba.

Pero no tendrá las propiedades definitorias clave de una prueba; fallando cuando las cosas están mal, pasando cuando están bien.

Esto se aplica a cualquier llamada estática, llamadas de constructor y cualquier referencia a métodos o campos no heredados de una clase o interfaz base . Si el nombre de la clase aparece en el código, es una dependencia visible para el compilador y no puede realizar una prueba válida sin él. Cualquier fragmento más pequeño simplemente no es una unidad comprobable válida . Cualquier intento de tratarlo como si tuviera resultados no será más significativo que escribir una pequeña utilidad para emitir el XML utilizado por su marco de prueba para decir 'prueba aprobada'.

Dado eso, hay tres opciones:

  1. defina la prueba de unidad como prueba de la unidad compuesta por una clase y sus dependencias codificadas. Esto funciona, siempre que evite dependencias circulares.

  2. nunca cree dependencias en tiempo de compilación entre clases que es responsable de probar. Esto funciona, siempre que no te importe el estilo de código resultante.

  3. no prueba de unidad, prueba de integración en su lugar. Lo que funciona, siempre que no entre en conflicto con algo más para lo que necesite usar el término prueba de integración.

soru
fuente
3
No hay nada que diga que una prueba unitaria es una prueba de una sola clase. Es una prueba de una sola unidad. Las propiedades definitorias de las pruebas unitarias son que son rápidas, repetibles e independientes. Math.PiHacer referencia a un método no lo convierte en una prueba de integración por ninguna definición razonable.
sara
es decir, opción 1. Estoy de acuerdo en que es el mejor enfoque, pero puede ser útil saber que otras personas usan los términos de manera diferente (razonablemente o no).
soru
4

No hay dos formas de hacerlo. Las sugerencias de ReSharper y varias características útiles de C # no se usarían con tanta frecuencia si estuviera escribiendo pruebas de unidades atómicas aisladas para todo su código.

Por ejemplo, si tiene un método estático y necesita eliminarlo, no puede hacerlo a menos que use un marco de aislamiento basado en perfil. Una solución alternativa compatible con llamadas es cambiar la parte superior del método para usar la notación lambda. Por ejemplo:

ANTES DE:

    public static DBConnection ConnectToDB( string dbName, string connectionInfo ) {
    }

DESPUÉS:

    public static Func<string, string, DBConnection> ConnectToDB (dbName, connectionInfo ) {
    };

Los dos son compatibles con llamadas. Las personas que llaman no tienen que cambiar. El cuerpo de la función sigue siendo el mismo.

Luego, en su código de Prueba de unidad, puede anular esta llamada de esta manera (suponiendo que esté en una clase llamada Base de datos):

        Database.ConnectToDB = (dbName, connectionInfo) => { return null|whatever; }

Tenga cuidado de reemplazarlo con el valor original una vez que haya terminado. Puede hacerlo mediante un intento / finalmente o, en la limpieza de la prueba de la unidad, la que se llama después de cada prueba, escriba un código como este:

    [TestCleanup]
    public void Cleanup()
    {
        typeof(Database).TypeInitializer.Invoke(null, null);
    }

que volverá a invocar el inicializador estático de su clase.

Las funciones Lambda no son tan ricas en soporte como los métodos estáticos regulares, por lo que este enfoque tiene los siguientes efectos secundarios indeseables:

  1. Si el método estático era un método de extensión, primero debe cambiarlo a un método sin extensión. Resharper puede hacer esto por usted automáticamente.
  2. Si alguno de los tipos de datos de los métodos estáticos es un ensamblado de interoperabilidad incrustado, como para Office, debe ajustar el método, ajustar el tipo o cambiarlo a tipo 'objeto'.
  3. Ya no puede usar la herramienta de refactorización de cambio de firma de Resharper.

Pero supongamos que evita las estáticas por completo, y convierte esto a un método de instancia. Todavía no se puede imitar a menos que el método sea virtual o esté implementado como parte de una interfaz.

Entonces, en realidad, cualquiera que sugiera el remedio para tropezar con métodos estáticos es hacerlos métodos de instancia, también estarían en contra de los métodos de instancia que no son virtuales o no son parte de una interfaz.

Entonces, ¿por qué C # tiene métodos estáticos? ¿Por qué permite métodos de instancia no virtuales?

Si utiliza cualquiera de estas "Características", simplemente no puede crear métodos aislados.

Entonces, ¿cuándo los usas?

Úselos para cualquier código que no espere que nadie quiera borrar. Algunos ejemplos: el método Format () de la clase String, el método WriteLine () de la clase Console, el método Cosh () de la clase Math

Y una cosa más ... A la mayoría de las personas no les importará esto, pero si puede hacerlo sobre el rendimiento de una llamada indirecta, esa es otra razón para evitar los métodos de instancia. Hay casos en que es un éxito de rendimiento. Es por eso que existen métodos no virtuales en primer lugar.

zumalifeguard
fuente
3
  1. Creo que es en parte porque los métodos estáticos son "más rápidos" para llamar que los métodos de instancia. (Entre comillas porque huele a micro optimización) vea http://dotnetperls.com/static-method
  2. Le dice que no necesita estado, por lo tanto, podría llamarse desde cualquier lugar, eliminando la sobrecarga de la instancia si eso es lo único que alguien necesita.
  3. Si quiero simularlo, entonces creo que generalmente es la práctica que se declara en una interfaz.
  4. Si se declara en una interfaz, R # no le sugerirá que sea estática.
  5. Si se declara virtual, R # tampoco te sugerirá que lo hagas estático.
  6. Mantener estado (campos) estáticamente es algo que siempre debe considerarse con cuidado . El estado estático y los hilos se mezclan como el litio y el agua.

R # no es la única herramienta que hará esta sugerencia. El análisis de código de FxCop / MS también hará lo mismo.

En general, diría que si el método es estático, en general también debería ser comprobable. Eso trae un poco de consideración de diseño y probablemente más discusión de la que tengo en mis dedos en este momento, esperando pacientemente los votos y comentarios negativos ...;)

desaparecido en combate
fuente
¿Se puede declarar un método / objeto estático en una interfaz? (Creo que no). De manera Ether, me refiero a cuando su método estático es llamado por el método bajo prueba. Si la persona que llama está en una unidad diferente, entonces el método estático debe aislarse. Esto es muy difícil de hacer sin una de las tres herramientas enumeradas anteriormente.
Vaccano
1
No, no puede declarar estática en una interfaz. No tendría sentido.
MIA
3

Veo que después de mucho tiempo, nadie ha declarado aún un hecho realmente simple. Si resharper me dice que puedo hacer que un método sea estático, significa algo enorme para mí, puedo escuchar su voz decirme: "hey, tú, estas piezas de lógica no son la RESPONSABILIDAD de manejar de la clase actual, por lo que deberían quedar fuera en alguna clase auxiliar o algo así ".

g1ga
fuente
2
Estoy en desacuerdo. La mayoría de las veces, cuando Resharper dice que puedo hacer algo estático, es un poco de código que era común a dos o más métodos en la clase y, por lo tanto, lo extraigo a su propio procedimiento. Mover eso a un ayudante sería una complejidad sin sentido.
Loren Pechtel
2
Puedo ver su punto solo si el dominio es muy simple y no es adecuado para futuras modificaciones. De manera diferente, lo que llamas "complejidad sin sentido" es un diseño bueno y legible para mí. Tener una clase auxiliar con una razón simple y clara para existir es, de alguna manera, el "mantra" de los principios de SoC y responsabilidad única. Además, teniendo en cuenta que esta nueva clase se convierte en una dependencia para la principal, tiene que exponer a algunos miembros públicos y, como consecuencia natural, se puede comprobar de forma aislada y fácil de burlarse cuando actúa como una dependencia.
g1ga
1
No relevante para la pregunta.
Igby Largeman
2

Si el método estático se llama desde otro método , no es posible evitar o sustituir dicha llamada. Esto significa que estos dos métodos forman una sola Unidad; La prueba de unidad de cualquier tipo los prueba a ambos.

Y si este método estático habla con Internet, conecta bases de datos, muestra ventanas emergentes de GUI o convierte la prueba de la Unidad en un desastre total, simplemente funciona sin ningún tipo de solución fácil. Un método que llama a este método estático no es comprobable sin refactorizar, incluso si tiene un montón de código puramente computacional que se beneficiaría enormemente de la prueba unitaria.

h22
fuente
0

Creo que Resharper le brinda orientación y aplica las pautas de codificación con las que se ha configurado. Cuando he usado Resharper y me ha dicho que un método debe ser estático, seguramente estará en un método privado que no actúa en ninguna variable de instancia.

Ahora, en cuanto a la capacidad de prueba, este escenario no debería ser un problema, ya que no debería probar métodos privados de todos modos.

En cuanto a la capacidad de prueba de los métodos estáticos que son públicos, las pruebas unitarias se vuelven difíciles cuando los métodos estáticos tocan el estado estático. Personalmente, mantendría esto al mínimo y usaría métodos estáticos como funciones puras tanto como sea posible cuando se pasen las dependencias al método que se pueden controlar a través de un dispositivo de prueba. Sin embargo, esta es una decisión de diseño.

aqwert
fuente
Me refiero a cuando tienes un método bajo prueba (no estático) que llama a un método estático en otra clase. Esa dependencia solo se puede aislar con una de las tres herramientas enumeradas anteriormente. Si no lo aísla, está realizando pruebas de integración, no pruebas unitarias.
Vaccano
Acceder a las variables de instancia inherentemente significa que no puede ser estático. El único estado que puede tener un método estático son las variables de clase y la única razón por la que debería suceder es si se trata de un singleton y, por lo tanto, no tiene otra opción.
Loren Pechtel el