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.
En el contexto de la prueba unitaria, una unidad tiene varias características.
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:
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.
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:
SqlClient
instancia de proveedor de fábrica.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.
¿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..
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:
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..
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.
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".
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.
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.
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:
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.
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.
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 PruebasAdemá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.
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..
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.
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..
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:
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.