Pruebas unitarias sucintamente ¿Qué son las pruebas unitarias?

Este es un extracto del eBook de Unit Testing Succinctly, por Marc Clifton, amablemente proporcionado por Syncfusion.

La prueba de unidad se trata de demostrar la corrección. Para probar que algo está funcionando correctamente, primero debe comprender qué unidad y un prueba en realidad son antes de que pueda explorar lo que es demostrable dentro de las capacidades de las pruebas unitarias.

Que es una unidad?

En el contexto de la prueba unitaria, una unidad tiene varias características.

Unidades puras

Una unidad pura es el método más fácil e ideal para escribir una prueba unitaria. Una unidad pura tiene varias características que facilitan las pruebas..

Una unidad no debería (idealmente) llamar a otros métodos

Con respecto a la prueba de unidad, una unidad debe ser ante todo un método que haga algo sin tener que llamar a ningún otro método. Ejemplos de estas unidades puras se pueden encontrar en el Cuerda y Mates Clases: la mayoría de las operaciones realizadas no se basan en ningún otro método. Por ejemplo, el siguiente código (tomado de algo que el autor ha escrito)

public void SelectedMasters () string currentEntity = dgvModel.DataMember; cadena navToEntity = cbMasterTables.SelectedItem.ToString (); DataGridViewSelectedRowCollection selectedRows = dgvModel.SelectedRows; StringBuilder qualifier = BuildQualifier (selectedRows); UpdateGrid (navToEntity); SetRowFilter (navToEntity, qualifier.ToString ()); ShowNavigateToMaster (navToEntity, qualifier.ToString ()); 

No debe considerarse una unidad por tres razones:

  • En lugar de tomar parámetros, obtiene los valores involucrados en el cálculo de los objetos de la interfaz de usuario, específicamente un DataGridView y un ComboBox.
  • Hace varias llamadas a otros métodos que potencialmente son unidades.
  • Uno de los métodos parece actualizar la pantalla, enredando un cálculo con una visualización.

La primera razón por la que se señala una propiedad de problema sutil debe considerarse como una llamada a un método. De hecho, están en la implementación subyacente. Si su método utiliza propiedades de otras clases, este es un tipo de llamada de método y debe considerarse cuidadosamente al escribir una unidad adecuada.

De manera realista, esto no siempre es posible. Con frecuencia, se requiere una llamada al marco o alguna otra API para que la unidad realice su trabajo con éxito. Sin embargo, estas llamadas deben ser inspeccionadas para determinar si el método podría mejorarse para hacer una unidad más pura, por ejemplo, extrayendo las llamadas a un método más alto y pasando los resultados de las llamadas como un parámetro a la unidad.

Una unidad debe hacer solo una cosa

Un corolario de "una unidad no debe llamar a otros métodos" es que una unidad es un método que hace una cosa y solo una cosa. A menudo se llaman otros métodos para hacer mas de una cosa-una habilidad valiosa para saber cuándo algo consiste realmente en varias subtareas, incluso si se puede describir como una tarea de alto nivel, lo que hace que parezca una tarea única!

El siguiente código puede parecer una unidad razonable que hace una cosa: inserta un nombre en la base de datos.

Insertar int público (Persona persona) DbProviderFactory factory = SqlClientFactory.Instance; utilizando (DbConnection connection = factory.CreateConnection ()) connection.ConnectionString = "Server = localhost; Database = myDataBase; Trusted_Connection = True;"; conexión.Abierta (); utilizando (comando DbCommand = connection.CreateCommand ()) command.CommandText = "inserte en los valores de PERSONA (ID, NOMBRE) (@Id, @Name)"; command.CommandType = CommandType.Text; DbParameter id = command.CreateParameter (); id.ParameterName = "@Id"; id.DbType = DbType.Int32; id.Value = person.Id; DbParameter name = command.CreateParameter (); name.ParameterName = "@Name"; name.DbType = DbType.String; name.Size = 50; name.Value = person.Name; command.Parameters.AddRange (nuevo DbParameter [] id, name); int rowsAffected = command.ExecuteNonQuery (); filas de retorno Afectadas; 

Sin embargo, este código en realidad está haciendo varias cosas:

  • Obteniendo un SqlClient instancia de proveedor de fábrica.
  • Instalando una conexión y abriéndola..
  • Instalar un comando e inicializar el comando.
  • Creando y agregando dos parámetros al comando.
  • Ejecutando el comando y devolviendo el número de filas afectadas.

Hay una variedad de problemas con este código que lo descalifican de ser una unidad y dificulta su reducción a unidades básicas. Una mejor manera de escribir este código podría verse así:

public int RefactoredInsert (Person person) DbProviderFactory factory = SqlClientFactory.Instance; using (DbConnection conn = OpenConnection (factory, "Server = localhost; Database = myDataBase; Trusted_Connection = True;")) using (DbCommand cmd = CreateTextCommand (conn, "inserte en los valores de PERSON (ID, NAME) (@Id, @ Nombre) ")) AddParameter (cmd," @Id ", person.Id); AddParameter (cmd, "@Name", 50, person.Name); int rowsAffected = cmd.ExecuteNonQuery (); filas de retorno Afectadas;  protegido DbConnection OpenConnection (DbProviderFactory factory, string connectString) DbConnection conn = factory.CreateConnection (); conn.ConnectionString = connectString; conn.Open (); volver conn;  protegido DbCommand CreateTextCommand (DbConnection conn, string cmdText) DbCommand cmd = conn.CreateCommand (); cmd.CommandText = cmdText; cmd.CommandType = CommandType.Text; return cmd;  AddParameter void protegido (DbCommand cmd, string ParamName, int paramValue) DbParameter param = cmd.CreateParameter (); param.ParameterName = paramName; param.DbType = DbType.Int32; param.Value = paramValue; cmd.Parameters.Add (param);  AddParameter void protegido (DbCommand cmd, string ParamName, int size, string paramValue) DbParameter param = cmd.CreateParameter (); param.ParameterName = paramName; param.DbType = DbType.String; param.Size = tamaño; param.Value = paramValue; cmd.Parameters.Add (param); 

Fíjate cómo, además de lucir más limpio, los métodos. OpenConnection, CreateTextCommand, y AddParameter son más adecuados para pruebas de unidad (ignorando el hecho de que son métodos protegidos). Estos métodos solo hacen una cosa y, como unidades, se pueden probar para garantizar que hagan esa única cosa correctamente. A partir de esto, hay poco sentido para probar el RefactoredInsert Método, ya que se basa totalmente en otras funciones que tienen pruebas unitarias. En el mejor de los casos, uno podría querer escribir algunos casos de prueba de manejo de excepciones, y posiblemente alguna validación en los campos en el Persona mesa.

Código correctamente comprobado

¿Qué sucede si el método de nivel superior hace algo más que simplemente llamar a otros métodos para los cuales existen pruebas unitarias, por ejemplo, algún tipo de cálculo adicional? En ese caso, el código que realiza el cálculo debe moverse a su propio método, las pruebas deben escribirse para él y, nuevamente, el método de nivel superior puede confiar en la corrección del código al que llama. Este es el proceso de construcción de un código correctamente correcto. La corrección de los métodos de nivel superior mejora cuando lo único que hacen es llamar a los métodos de nivel inferior que tienen pruebas (pruebas unitarias) de la corrección..

Una unidad no debería (idealmente) tener múltiples rutas de código

La complejidad ciclomática es la perdición de las pruebas unitarias y las pruebas de aplicaciones en general, ya que aumenta la dificultad de probar todas las rutas de código. Idealmente, una unidad no tendrá ningún Si o cambiar declaraciones El cuerpo de esas declaraciones se debe considerar como las unidades (suponiendo que cumplan con los otros criterios de una unidad) y para que puedan hacerse verificables, se deben extraer en sus propios métodos..

Aquí hay otro ejemplo tomado del proyecto MyXaml del autor (parte del analizador):

if (tagName == "*") foreach (nodo XmlNode en topElement.ChildNodes) if (! (el nodo es XmlComment)) objectNode = node; descanso;  foreach (XmlAttribute attr en objectNode.Attributes) if (attr.LocalName == "Name") nameAttr = attr; descanso;  else else … etc…

Aquí tenemos múltiples rutas de código que involucran Si, más, y para cada declaraciones, que:

  • Cree complejidad de configuración, ya que se deben cumplir muchas condiciones para ejecutar el código interno.
  • Cree una complejidad de prueba, ya que las rutas de código requieren configuraciones diferentes para garantizar que se pruebe cada ruta de código.

Obviamente, la bifurcación condicional, los bucles, las declaraciones de casos, etc. no pueden evitarse, pero puede valer la pena considerar la refactorización del código para que los aspectos internos de las condiciones y los bucles sean métodos independientes que puedan probarse de forma independiente. Luego, las pruebas para el método de nivel superior pueden simplemente garantizar que los estados (representados por condiciones, bucles, interruptores, etc.) se manejen de manera adecuada, independientemente de los cálculos que realicen..

Unidades Dependientes

Los métodos que dependen de otras clases, datos e información de estado son más complejos de probar porque esas dependencias se traducen en requisitos para objetos instanciados, existencia de datos y estado predeterminado.

Precondiciones

En su forma más simple, las unidades dependientes tienen condiciones previas que deben cumplirse. Los motores de pruebas unitarias proporcionan mecanismos para crear instancias de dependencias de prueba, tanto para pruebas individuales como para todas las pruebas dentro de un grupo de prueba o "accesorio".

Servicios reales o simulados

Las unidades dependientes complicadas requieren servicios como las conexiones de base de datos para ser instanciadas o simuladas. En el ejemplo de código anterior, Insertar El método no puede probarse por unidades sin la capacidad de conectarse a una base de datos real. Este código se vuelve más comprobable si se puede simular la interacción de la base de datos, generalmente mediante el uso de interfaces o clases base (resumen o no).

Los métodos refactorizados en el Insertar El código descrito anteriormente es un buen ejemplo porque DbProviderFactory es una clase base abstracta, por lo que uno puede crear fácilmente una clase derivada de DbProviderFactory para simular la conexión de base de datos.

Manejo de excepciones externas

Las unidades dependientes, debido a que están realizando llamadas a otras API o métodos, también son más frágiles: es posible que deban manejar explícitamente los errores potencialmente generados por los métodos a los que llaman. En el ejemplo de código anterior, Insertar El código del método podría estar envuelto en un bloque try-catch, porque ciertamente es posible que la conexión de la base de datos no exista. El controlador de excepciones podría devolver 0 para el número de filas afectadas, informar el error a través de algún otro mecanismo. En tal escenario, las pruebas unitarias deben ser capaces de simular esta excepción para garantizar que todas las rutas de código se ejecuten correctamente, incluyendo captura y finalmente bloques.


Que es una prueba?

Una prueba proporciona una afirmación útil de la corrección de la unidad. Las pruebas que afirman la corrección de una unidad normalmente ejercen la unidad de dos maneras:

  • Prueba de cómo se comporta la unidad en condiciones normales..
  • Prueba de cómo se comporta la unidad en condiciones anormales..

Pruebas de condiciones normales

Probar cómo se comporta la unidad en condiciones normales es, con mucho, la prueba más fácil de escribir. Después de todo, cuando escribimos una función, o bien la estamos escribiendo para satisfacer un requisito explícito o implícito. La implementación refleja una comprensión de ese requisito, que en parte abarca lo que esperamos como entradas a la función y cómo esperamos que la función se comporte con esas entradas. Por lo tanto, estamos probando el resultado de la función dada las entradas esperadas, ya sea que el resultado de la función sea un valor de retorno o un cambio de estado. Además, si la unidad depende de otras funciones o servicios, también esperamos que se comporten correctamente y estamos escribiendo una prueba con esa suposición implícita.

Pruebas de condiciones anormales

Probar cómo se comporta la unidad en condiciones anormales es mucho más difícil. Requiere determinar qué es una condición anormal, que generalmente no es evidente al inspeccionar el código. Esto se hace más complicado cuando se prueba una unidad dependiente, una unidad que está esperando que otra función o servicio se comporte correctamente. Además, no sabemos cómo otro programador o usuario puede ejercer la unidad.


Pruebas unitarias y otras prácticas de prueba

Pruebas unitarias como parte de un enfoque de prueba integral

La prueba de unidad no reemplaza otras prácticas de prueba; Debe complementar otras prácticas de prueba, proporcionando soporte de documentación adicional y confianza. La Figura 1 ilustra un concepto del "flujo de desarrollo de aplicaciones": cómo otras pruebas se integran con las pruebas unitarias. Tenga en cuenta que el cliente puede participar en cualquier etapa, aunque generalmente en el procedimiento de prueba de aceptación (ATP), la integración del sistema y las etapas de usabilidad..

Compare esto con el modelo V del proceso de desarrollo y prueba del software. Si bien está relacionado con el modelo de desarrollo de software en cascada (que, en última instancia, todos los demás modelos de desarrollo de software son un subconjunto o una extensión de), el modelo V proporciona una buena imagen de qué tipo de prueba se requiere para cada capa de El proceso de desarrollo de software:

El V-Modelo de Pruebas

Además, cuando un punto de prueba falla en alguna otra práctica de prueba, un fragmento específico de código generalmente se puede identificar como responsable de la falla. Cuando ese es el caso, es posible tratar esa pieza de código como una unidad y escribir una prueba de unidad para crear primero la falla y, cuando el código ha sido cambiado, verificar la corrección.

Procedimientos de prueba de aceptación

Un procedimiento de prueba de aceptación (ATP) se usa a menudo como un requisito contractual para probar que se ha implementado cierta funcionalidad. Los ATP a menudo se asocian con hitos y los hitos a menudo se asocian con pagos o financiamiento adicional del proyecto. Un ATP difiere de una prueba unitaria porque el ATP demuestra que se ha implementado la funcionalidad con respecto a todo el requisito de artículo de línea. Por ejemplo, una prueba unitaria puede determinar si el cálculo es correcto. Sin embargo, la ATP puede validar que los elementos de usuario se proporcionan en la interfaz de usuario y que la interfaz de usuario muestra el resultado del cálculo según lo especificado por el requisito. Estos requisitos no están cubiertos por la prueba unitaria..

Prueba automatizada de interfaz de usuario

Una ATP puede escribirse inicialmente como una serie de interacciones de interfaz de usuario (UI) para verificar que se cumplen los requisitos. Las pruebas de regresión de la aplicación a medida que continúa evolucionando son aplicables a las pruebas unitarias, así como a las pruebas de aceptación. La prueba de interfaz de usuario automatizada es otra herramienta completamente separada de la prueba de unidad que ahorra tiempo y mano de obra, al tiempo que reduce los errores de prueba. Al igual que con los ATP, las pruebas unitarias de ninguna manera reemplazan el valor de las pruebas de interfaz de usuario automatizadas.

Pruebas de usabilidad y experiencia del usuario

Las pruebas unitarias, los ATP y las pruebas automatizadas de UI no reemplazan de ninguna manera la prueba de usabilidad: poner la aplicación frente a los usuarios y obtener su retroalimentación de "experiencia de usuario". Las pruebas de usabilidad no deben consistir en encontrar defectos computacionales (errores) y, por lo tanto, están completamente fuera del ámbito de las pruebas unitarias..

Pruebas de rendimiento y carga

Algunas herramientas de prueba unitaria proporcionan un medio para medir el rendimiento de un método. Por ejemplo, el motor de prueba de Visual Studio informa sobre el tiempo de ejecución, y NUnit tiene atributos que se pueden usar para verificar que un método se ejecuta dentro de un tiempo asignado.

Idealmente, una herramienta de prueba unitaria para lenguajes .NET debería implementar explícitamente las pruebas de rendimiento para compensar la compilación de código justo a tiempo (JIT) la primera vez que se ejecuta el código.

La mayoría de las pruebas de carga (y las pruebas de rendimiento relacionadas) no son adecuadas para pruebas unitarias. Ciertas formas de pruebas de carga también se pueden realizar con pruebas unitarias, al menos según las limitaciones del hardware y el sistema operativo, como:

  • Simulando restricciones de memoria.
  • Simulación de restricciones de recursos..

Sin embargo, este tipo de pruebas idealmente requieren el soporte del marco de trabajo o la API del sistema operativo para simular este tipo de cargas para la aplicación que se está probando. Obligando a todo el sistema operativo a consumir una gran cantidad de memoria, recursos o ambos, afecta a todas las aplicaciones, incluida la aplicación de prueba de la unidad. Este no es un enfoque deseable..

Otros tipos de pruebas de carga, como simular múltiples instancias de ejecutar una operación simultáneamente, no son candidatos para pruebas de unidad. Por ejemplo, probablemente no sea posible probar el rendimiento de un servicio web con una carga de un millón de transacciones por minuto utilizando una sola máquina. Si bien este tipo de prueba se puede escribir fácilmente como una unidad, la prueba real involucraría un conjunto de máquinas de prueba. Y al final, solo ha probado un comportamiento muy estrecho del servicio web en condiciones de red muy específicas, que de ninguna manera representan el mundo real..

Por esta razón, las pruebas de rendimiento y carga tienen una aplicación limitada con las pruebas unitarias.