Un día en la vida de un desarrollador de ALM: Escribir código nuevo para un caso de usuario
Publicada: abril de 2016
¿Es un usuario nuevo de Visual Studio Application Lifecycle Management (ALM) y Team Foundation Server (TFS)? ¿Se pregunta cómo usted y su equipo pueden obtener el máximo beneficio de la versión más reciente de estas herramientas para compilar la aplicación?
Entonces, dedique unos minutos a seguir paso a paso este tutorial de dos capítulos y observe durante un día el trabajo de Peter y de Julia, dos desarrolladores de Fabrikam Fiber, una compañía que suministra televisión por cable y otros servicios afines. Verá ejemplos de cómo utilizar Visual Studio y TFS para desproteger y actualizar código, suspender el trabajo cuando le interrumpen, solicitar una revisión del código, proteger los cambios y realizar otras tareas.
La historia hasta ahora
El equipo comenzó recientemente la adopción de Visual Studio y Team Foundation Server para Application Lifecycle Management (ALM). Configuraron los equipos cliente y servidor, crearon un trabajo pendiente, planearon una iteración y completaron otros planes necesarios para iniciar el desarrollo de la aplicación.
Información general de este capítulo
Peter revisa brevemente el trabajo pendiente y selecciona la tarea en la que trabajará hoy. Escribe las pruebas unitarias para el código que planea desarrollar. Normalmente, ejecuta las pruebas varias veces en una hora, escribe paulatinamente pruebas más detalladas y, a continuación, escribe el código para superarlas. A menudo comenta la interfaz del código con los colegas que utilizarán el método que está escribiendo.
Nota
Las características Mi Trabajo y Cobertura de código descritas en este tema solo están disponibles en Visual Studio Premium y Visual Studio Ultimate.
En este tema
Revisar el trabajo pendiente personal y preparar las tareas para iniciar el trabajo
Crear la primera prueba unitaria
Crear código auxiliar para el nuevo código
Ejecutar la primera prueba
Aceptar la API
Rojo, verde, refactorizar…
Cobertura de código
¿Cuándo terminamos?
Proteger los cambios
Revisar el trabajo pendiente personal y preparar las tareas para iniciar el trabajo
En Team Explorer, Peter abre la página Mi trabajo. El equipo ha acordado que, durante el sprint actual, Peter trabajará en la evaluación del estado de la factura, un elemento prioritario en el trabajo pendiente del producto. Peter decide empezar la implementación de funciones matemáticas, una tarea secundaria del elemento de trabajo pendiente prioritario. Arrastra esta tarea desde la lista Elementos de trabajo disponibles hasta la lista Elementos de trabajo y cambios en curso.
Para revisar el trabajo pendiente personal y preparar las tareas para iniciar el trabajo
En Team Explorer:
Conéctese al proyecto de equipo en el que desea trabajar, si aún no lo está.
Elija Inicio y, a continuación, elija Mi trabajo.
En la página Mi trabajo, arrastre la tarea desde la lista de Elementos de trabajo disponibles hasta la sección Elementos de trabajo en curso.
También se puede seleccionar una tarea en la lista Elementos de trabajo disponibles y, a continuación, elegir Iniciar.
Borrador de plan de trabajo incremental
Peter normalmente desarrolla código en una serie de pequeños pasos. Normalmente, cada paso se prolonga no más de una hora y puede llevar solo diez minutos. En cada paso, escribe una nueva prueba unitaria y cambia el código que está desarrollando para que pase la nueva prueba, además de las pruebas que ya ha escrito. Unas veces Peter escribe la nueva prueba antes de cambiar el código y, otras veces, cambia el código antes de escribir la prueba. A veces refactoriza. Es decir, simplemente mejora el código sin agregar nuevas pruebas. Peter nunca cambia una prueba que se ha superado, a menos que decida que no representaba correctamente un requisito.
Al final de cada pequeño paso ejecuta todas las pruebas unitarias que son relevantes para esta área del código. No considera que el paso se ha completado hasta que se supera cada prueba.
Sin embargo, no protegerá el código en Team Foundation Server hasta que haya finalizado toda la tarea.
Peter diseña un plan preliminar para esta secuencia de pasos pequeños. Sabe que los detalles exactos y el orden de los últimos cambiarán probablemente mientras trabaja. Esta es la lista inicial de los pasos que debe seguir para esta tarea concreta:
Crear el código auxiliar de método, que es, simplemente, la signatura del método.
Satisfacer un caso típico concreto.
Probar un intervalo amplio. Asegurarse de que el código responde correctamente a un intervalo amplio de valores.
Iniciar una excepción si el resultado es negativo. Tratar eficazmente los parámetros incorrectos.
Cobertura de código. Asegurarse de que las pruebas unitarias utilizan al menos el 80 % del código.
Algunos de sus compañeros escriben este tipo de plan en los comentarios del código de prueba. Otros simplemente lo memorizan. Peter cree que es muy útil escribir la lista de pasos en el campo Descripción del elemento de trabajo Tarea. Si tuviera que cambiar y hacer temporalmente una tarea más urgente, sabría dónde encontrar la lista cuando pueda volver a ella.
Crear la primera prueba unitaria
Peter comienza con la creación de una prueba unitaria. Comienza con la prueba unitaria porque quiere escribir un ejemplo de código que utilice su nueva clase.
Se trata de la primera prueba unitaria para la biblioteca de clases que está probando, por lo que crea un nuevo proyecto de prueba unitaria. Abre el cuadro de diálogo Nuevo proyecto y elige Visual C#, Probar y, a continuación, Proyecto de prueba unitaria.
El proyecto de prueba unitaria proporciona un archivo de C# en el que puede escribir el ejemplo. En este fase, solo quiere mostrar cómo se invocará uno de sus nuevos métodos:
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Fabrikam.Math.UnitTest
{
[TestClass]
public class UnitTest1
{
[TestMethod]
// Demonstrates how to call the method.
public void SignatureTest()
{
// Create an instance:
var math = new Fabrikam.Math.LocalMath();
// Get a value to calculate:
double input = 0.0;
// Call the method:
double actualResult = math.SquareRoot(input);
// Use the result:
Assert.AreEqual(0.0, actualResult);
}
}
}
Escribe el ejemplo en un método de prueba porque, cuando haya escrito el código, quiere que el ejemplo funcione.
Para crear un proyecto de prueba unitaria y métodos
Normalmente, se crearía un nuevo proyecto de prueba para cada proyecto que se esté probando. Si ya existe un proyecto de prueba, puede agregar simplemente nuevas clases y nuevos métodos de prueba.
En este procedimiento se utiliza el marco de pruebas unitarias de Visual Studio, pero también se pueden utilizar marcos de trabajo de otros proveedores. El Explorador de pruebas funciona igual de bien con otros marcos, siempre que se instale el adaptador adecuado.
Cree un proyecto de prueba si no existe ya.
- En el cuadro de dialogo Nuevo proyecto, elija un lenguaje, como Visual Basic, Visual C++ o Visual C#. Elija Probar y, a continuación, Proyecto de prueba unitaria.
Agregue las pruebas a la clase de prueba que se proporciona. Cada prueba unitaria es un método.
Cada prueba unitaria debe ir precedida del atributo TestMethod y el método de prueba unitaria no debe tener ningún parámetro. Para un método de prueba unitaria, se puede utilizar cualquier nombre que se desee:
[TestMethod] public void SignatureTest() {...}
<TestMethod()> Public Sub SignatureTest() ... End Sub
Cada método de prueba debe llamar a un método de la clase Assert para indicar si se ha superado o no. Por lo general, se comprueba que los resultados previstos de una operación y los reales son iguales:
Assert.AreEqual(expectedResult, actualResult);
Assert.AreEqual(expectedResult, actualResult)
Los métodos de prueba pueden llamar a otros métodos normales que no tienen el atributo TestMethod.
Las pruebas se pueden organizar en más de una clase. Cada clase debe ir precedida por el atributo TestClass.
[TestClass] public class UnitTest1 { ... }
<TestClass()> Public Class UnitTest1 ... End Class
Para obtener más información sobre cómo escribir pruebas unitarias en C++, vea Escribir pruebas unitarias para C/C++ con el Framework de pruebas unitarias de Microsoft para C++.
Crear código auxiliar para el nuevo código
A continuación, Peter crea un proyecto de biblioteca de clases para el nuevo código. Ahora tenemos un proyecto para el código en desarrollo y otro proyecto para las pruebas unitarias. Peter agrega una referencia de proyecto desde el proyecto de prueba al código en desarrollo.
En el nuevo proyecto, agrega la nueva clase y una versión mínima del método que permitirá, por lo menos, que la prueba se compile correctamente. La forma más rápida de hacerlo es generar código auxiliar de clase y de método desde la invocación en la prueba.
public double SquareRoot(double p)
{
throw new NotImplementedException();
}
Para generar las clases y los métodos de las pruebas
En primer lugar, cree el proyecto al que desea agregar la nueva clase, a menos que este ya exista.
Para generar una clase
Coloque el cursor en un ejemplo de la clase que desea generar, por ejemplo, LocalMath. En el menú contextual, elija Generar código, Nuevo tipo.
En el cuadro de diálogo Nuevo tipo, establezca Proyecto en el proyecto de biblioteca de clases. En este ejemplo, es Fabrikam.Math.
Para generar un método
- Coloque el cursor en una llamada al método, por ejemplo, SquareRoot. En el menú contextual, elija Generar código, Código auxiliar de método.
Ejecutar la primera prueba
Peter compila y ejecuta la prueba presionando CTRL+R, T. El resultado de pruebas muestra un indicador en rojo Error y la prueba aparece bajo la lista de Pruebas no superadas.
Realiza un cambio sencillo en el código:
public double SquareRoot(double p)
{
return 0.0;
}
Vuelve a ejecutar la prueba y esta se supera:
Para ejecutar pruebas unitarias
En el menú Prueba, elija Ejecutar, Todas las pruebas.
--O bien--
Si el Explorador de pruebas está abierto, elija Ejecutar todas.
-O bien-
Coloque el cursor en un archivo de código de prueba y presione CTRL+R, T.
Si aparece una prueba en Pruebas no superadas:
Abra la prueba, por ejemplo, haciendo doble clic en el nombre.
Se muestra el punto en el que la prueba dio error.
Para ver una lista completa de pruebas, elija Mostrar todo. Para volver al resumen, elija la vista Inicio.
Para ver los detalles de un resultado de pruebas, seleccione la prueba en el Explorador de pruebas.
Para navegar al código de una prueba, haga doble clic en la prueba en el Explorador de pruebas o elija Abrir prueba en el acceso directo.
Para depurar una prueba, abra el acceso directo de una o varias pruebas y, a continuación, elija Depurar pruebas seleccionadas.
Para ejecutar pruebas en segundo plano cada vez que se compila la solución, seleccione Ejecutar pruebas después de compilar. Las pruebas que no se superaron anteriormente se ejecutan primero.
Aceptar la interfaz
Peter llama a su compañera Julia en Lync y comparte la pantalla. Ella utilizará el componente. Él le muestra el ejemplo inicial.
Julia piensa que el ejemplo está bien, pero comenta, “Muchas funciones pasarían esa prueba”.
Peter responde, "La primera prueba es solo para asegurarse de que el nombre y los parámetros de la función son correctos. Ahora podemos escribir una prueba que capture el requisito principal de esta función".
Juntos escriben la siguiente prueba:
[TestMethod]
public void QuickNonZero()
{
// Create an instance to test:
LocalMath math = new LocalMath();
// Create a test input and expected value:
var expectedResult = 4.0;
var inputValue = expectedResult * expectedResult;
// Run the method:
var actualResult = math.SquareRoot(inputValue);
// Validate the result:
var allowableError = expectedResult/1e6;
Assert.AreEqual(expectedResult, actualResult, allowableError,
"{0} is not within {1} of {2}", actualResult, allowableError, expectedResult);
}
Sugerencia
Para esta función, Peter utiliza Probar primer desarrollo, en el que primero escribe la prueba unitaria para una característica y luego escribe el código que supera la prueba.En otros casos, considera que esta práctica no es realista, así que escribe las pruebas después de escribir el código.Pero considera muy importante escribir pruebas unitarias, ya sea antes o después del código, porque estas mantienen el código estable.
Rojo, verde, refactorizar…
Peter sigue un ciclo en el que escribe repetidamente una prueba y confirma que no se puede realizar, escribe código para lograr que la prueba se supere y luego considera la refactorización, es decir, mejorar el código sin cambiar las pruebas.
Rojo
Peter presiona CTRL + R, T para ejecutar la nueva prueba que creó con Julia. Después de escribir cualquier prueba, Peter siempre la ejecuta para asegurarse de que no se puede realizar antes de escribir el código que permite superarla. Esta es una práctica que aprendió una vez que olvidó colocar aserciones en algunas pruebas que había escrito. Ver el resultado de error le da la seguridad de que, cuando consiga superar la prueba, el resultado de la misma indicará correctamente que se ha cumplido un requisito.
Otra práctica útil es establecer Ejecutar pruebas después de compilar. Esta opción ejecuta las pruebas en segundo plano cada vez que se compila la solución, de forma que se obtiene un informe continuo del estado de la prueba del código. Peter sospechaba al principio que esto haría que Visual Studio respondiera lentamente, pero ha observado que esto raramente ocurre.
Verde
Peter hace un primer intento de escribir el código del método que está desarrollando:
public class LocalMath
{
public double SquareRoot(double x)
{
double estimate = x;
double previousEstimate = -x;
while (System.Math.Abs(estimate - previousEstimate) > estimate / 1000)
{
previousEstimate = estimate;
estimate = (estimate * estimate - x) / (2 * estimate);
}
return estimate;
}
Peter ejecuta las pruebas de nuevo y se superan todas:
Refactorizar
Ahora que el código realiza su función principal, Peter lo examina para buscar el modo de que funcione mejor o para facilitar los posibles cambios en el futuro. Se da cuenta de que puede reducir el número de cálculos realizados en el bucle:
public class LocalMath
{
public double SquareRoot(double x)
{
double estimate = x;
double previousEstimate = -x;
while (System.Math.Abs(estimate - previousEstimate) > estimate / 1000)
{
previousEstimate = estimate;
estimate = (estimate + x / estimate) / 2;
//was: estimate = (estimate * estimate - x) / (2 * estimate);
}
return estimate;
}
Comprueba que las pruebas se siguen superando:
Sugerencia
Cada cambio que se realiza mientras se desarrolla el código debe ser una refactorización o una extensión:
-
La refactorización significa que no se modifican las pruebas porque no se va a agregar ninguna funcionalidad nueva.
-
La extensión significa agregar pruebas y realizar los cambios de código que sean necesarios para superar las pruebas nuevas y las existentes.
Si está actualizando el código existente de los requisitos que han cambiado, eliminará también las pruebas anteriores que ya no representan los requisitos actuales.
Evite cambiar las pruebas que ya se han superado.En su lugar, agregue nuevas pruebas.Escriba solo las pruebas que representen un requisito real.
Ejecute las pruebas después de cada cambio.
… y repetir el proceso
Peter continúa la serie de pasos de extensión y refactorización, utilizando la lista de pequeños pasos como guía básica. No siempre realiza un paso de refactorización después de cada extensión y, a veces, realiza más de un paso de refactorización consecutivo. Pero siempre ejecuta las pruebas unitarias después de cada cambio en el código.
A veces agrega una prueba que no requiere cambios en el código, pero que aumenta su confianza de que el código funciona correctamente. Por ejemplo, quiere asegurarse de que la función opera con una amplia gama de entradas. Escribe más pruebas, como la siguiente:
[TestMethod]
public void SqRtValueRange()
{
LocalMath math = new LocalMath();
for (double expectedResult = 1e-8;
expectedResult < 1e+8;
expectedResult = expectedResult * 3.2)
{
VerifyOneRootValue(math, expectedResult);
}
}
private void VerifyOneRootValue(LocalMath math, double expectedResult)
{
double input = expectedResult * expectedResult;
double actualResult = math.SquareRoot(input);
Assert.AreEqual(expectedResult, actualResult, expectedResult / 1e6);
}
Esta prueba se supera la primera vez que se ejecuta:
Solo para asegurarse de que este resultado no es un error, introduce temporalmente un pequeño error en la prueba para que no se supere. Después de ver el error, corrige otra vez el código.
Sugerencia
Haga siempre que una prueba dé error antes de aprobarla.
Excepciones
A continuación, Peter pasa a escribir pruebas para entradas excepcionales:
[TestMethod]
public void RootTestNegativeInput()
{
LocalMath math = new LocalMath();
try
{
math.SquareRoot(-10.0);
}
catch (ArgumentOutOfRangeException)
{
return;
}
catch
{
Assert.Fail("Wrong exception on negative input");
return;
}
Assert.Fail("No exception on negative input");
}
Esta prueba coloca el código en un bucle. Tiene que usar el botón Cancelar del Explorador de pruebas. Esto finaliza el código dentro de 10 segundos.
Peter quiere asegurarse de que no se pueda producir un bucle sin fin en el servidor de compilación. Aunque el servidor impone un tiempo de espera a una ejecución completa, es un tiempo de espera muy prolongado y causaría retrasos significativos. Por lo tanto, agrega un tiempo de espera explícito para esta prueba:
[TestMethod, Timeout(1000)]
public void RootTestNegativeInput()
{...
El tiempo de espera explícito hace que la prueba no se supere.
A continuación, Peter actualiza el código para tratar este caso excepcional:
public double SquareRoot(double x)
{
if (x <= 0.0)
{
throw new ArgumentOutOfRangeException();
}
Regresión
La nueva prueba se supera, pero hay una regresión. Una prueba que solía aprobarse ahora no se supera:
Peter encuentra y corrige el error:
public double SquareRoot(double x)
{
if (x < 0.0) // not <=
{
throw new ArgumentOutOfRangeException();
}
Una vez corregido, se superan todas las pruebas:
Sugerencia
Asegúrese de que se supera cada prueba después de cada cambio realizado en el código.
Cobertura de código
A intervalos durante su trabajo y, por último, antes de proteger el código, Peter obtiene un Informe de cobertura de código, que muestra la cantidad de código ejecutado en las pruebas.
El equipo de Pedro tiene como objetivo una cobertura de al menos el 80 %. Han relajado este requisito para el código generado, porque puede ser difícil lograr una gran cobertura para este tipo de código.
Una buena cobertura no es garantía de que se haya probado toda la funcionalidad del componente, ni tampoco garantiza que el código funcione para cada intervalo de valores de entrada. Sin embargo, hay una correlación bastante estrecha entre la cobertura de las líneas de código y la cobertura del espacio de comportamiento de un componente. Por lo tanto, una buena cobertura refuerza la confianza del equipo en que está probando la mayor parte del comportamiento que tenía que probar.
Para obtener un informe de cobertura de código, en el menú Pruebas, elija Ejecutar, Analizar cobertura de código para todas las pruebas. A continuación, ejecute de nuevo todas las pruebas.
Peter obtiene una cobertura total del 86 %. Cuando expande el total en el informe, este muestra que el código que está desarrollando tiene una cobertura del 100 %. Esto es muy satisfactorio, porque la calificación importante corresponde al código en pruebas. Las secciones no cubiertas están realmente en las propias pruebas. Al alternar el botón Mostrar colores en cobertura de código, Peter puede ver qué partes del código de prueba no se han utilizado. Sin embargo, decide que estas secciones no son importantes para la cobertura porque están en el código de prueba y solo se utilizan si se detecta un error.
Para comprobar que una prueba concreta llega a bifurcaciones del código específicas, se puede establecer Mostrar colores en cobertura de código y ejecutar esa sola prueba con el comando Ejecutar en el menú contextual.
¿Cuándo terminamos?
Peter continúa actualizando el código en pequeños pasos hasta que está convencido de que:
Se aprueban todas las pruebas unitarias disponibles.
En un proyecto con un gran conjunto de pruebas unitarias, puede ser inviable que un desarrollador espere a que se ejecuten todas las pruebas. Como alternativa, el proyecto opera un servicio de protección controlada, en el que todas las pruebas automatizadas se ejecutan para cada conjunto de cambios aplazados protegido antes de combinarlo con el árbol de código fuente. La protección se rechaza si se produce un error en la ejecución. Este hecho permite al desarrollador ejecutar un conjunto mínimo de pruebas unitarias en su propio equipo y, a continuación, seguir con el trabajo sin riesgo de interrumpir la compilación. Para obtener más información, vea Utilizar un proceso de compilación de protección controlada para validar cambios.
La cobertura de código cumple con los estándares del equipo. El 75 % es un requisito normal del proyecto.
Sus pruebas unitarias simulan todos los aspectos del comportamiento que se requiere, incluidas las entradas típicas y las excepcionales.
Su código es fácil de entender y de extender.
Cuando se cumplan todos estos criterios, Peter estará preparado para proteger el código en el control de código fuente.
Principios de desarrollo de código con pruebas unitarias
Cuando desarrolla código, Peter aplica los siguientes principios:
Desarrollar las pruebas unitarias junto con el código y ejecutarlas con frecuencia durante el desarrollo. Las pruebas unitarias representan la especificación del componente.
No cambiar las pruebas unitarias, a menos que los requisitos hayan cambiado o las pruebas fueran incorrectas. Agregar las nuevas pruebas paulatinamente, a medida que se extiende la funcionalidad del código.
Intentar que las pruebas cubran al menos el 75 % del código. Examinar los resultados de cobertura de código a intervalos y antes de proteger el código fuente.
Proteger las pruebas unitarias junto con el código, para que puedan ejecutarse en las compilaciones normales o continuas del servidor.
Si es viable, para cada parte de funcionalidad, escribir la prueba unitaria en primer lugar. Esto debe hacerse antes de desarrollar el código que la cumple.
Proteger los cambios
Antes de proteger los cambios, Peter vuelve a utilizar Lync para compartir la pantalla con su colega Julia, para que ella pueda revisar con él de manera informal e interactiva lo que ha creado. Las pruebas siguen siendo el centro de su discusión porque Julia está principalmente interesada en lo que el código hace, no en cómo funciona. Julia está de acuerdo en que lo que Peter ha escrito satisface sus necesidades.
Peter protege todos los cambios que ha realizado, incluidos los de las pruebas y del código, y los asocia a la tarea que ha completado. El proceso de protección pone en cola el sistema automatizado de compilación del equipo con objeto de validar los cambios mediante el proceso de compilación de integración continua del equipo. Este proceso de compilación ayuda al equipo a minimizar los errores en el código base al compilar y probar, en un entorno limpio separado de los equipos de desarrollo, cada cambio que el equipo hace.
A Peter le notifican cuando se ha completado la compilación. En la ventana de resultados de la compilación, observa que la compilación se realizó correctamente y que todas las pruebas se aprobaron.
Para proteger los cambios
En la barra de menús, elija Vista, Team Explorer.
En Team Explorer, elija Inicio y, después, elija Mi trabajo.
En la página Mi trabajo, elija Proteger.
Revise el contenido de la página Cambios pendientes para asegurarse de que:
Todos los cambios pertinentes aparecen en Cambios incluidos.
Todos los elementos de trabajo relevantes aparecen en Elementos de trabajo relacionados.
Especifique un Comentario para ayudar al equipo a comprender el propósito de estos cambios cuando examinen el historial del control de versiones en los archivos y carpetas modificados.
Elija Proteger.
Para integrar continuamente el código
Para obtener más información sobre cómo definir un proceso de compilación de integración continua, consulte Configurar una compilación de integración continua. Tras haber configurado este proceso de compilación, puede elegir que le notifiquen los resultados de las compilaciones del equipo.
Para obtener más información, vea Ejecutar, supervisar y administrar compilaciones.
Siguiente (suspender el trabajo, corregir un error y llevar a cabo una revisión de código)