Un día en la vida de un desarrollador de DevOps: escribir código nuevo para un caso de usuario

Azure DevOps Services | Azure DevOps Server 2022 | Azure DevOps Server 2019

Visual Studio 2019 | Visual Studio 2022

En este tutorial se explica cómo usted y su equipo pueden beneficiarse de las versiones más recientes del Control de versiones de Team Foundation (TFVC) y Visual Studio para compilar una aplicación. El tutorial proporciona ejemplos de cómo utilizar Visual Studio y TFS para extraer del repositorio y actualizar código, suspender el trabajo cuando le interrumpen, solicitar una revisión del código, insertar en el repositorio los cambios y realizar otras tareas.

Cuando un equipo adopta Visual Studio y TFVC para administrar su código, configuran sus máquinas cliente y servidor, crean un registro de trabajo pendiente, planean una iteración y completan todo el otro planeamiento necesario para empezar a desarrollar su aplicación.

Los desarrolladores revisan sus trabajos pendientes para seleccionar las tareas en las que trabajar. Escriben pruebas unitarias para el código que planean desarrollar. Normalmente, ejecutan las pruebas varias veces en una hora, escriben paulatinamente pruebas más detalladas y, a continuación, escriben el código para superarlas. A menudo, los desarrolladores describen sus interfaces de código con compañeros que usarán el método que están escribiendo.

Las herramientas Mi trabajo y Revisión de código de Visual Studio ayudan a los desarrolladores a administrar su trabajo y colaborar con compañeros.

Nota:

Las características Mi trabajo y Revisión del código de Visual Studio están disponibles con las ediciones siguientes:

  • Visual Studio 2022: Visual Studio Community, Visual Studio Professional y Visual Studio Enterprise
  • Visual Studio 2019: Visual Studio Professional y Visual Studio Enterprise

Revisión de elementos de trabajo y preparación para comenzar el trabajo

El equipo ha acordado que, durante el sprint actual, usted trabajará en la evaluación del estado de la factura, un elemento prioritario en el trabajo pendiente del producto. Decide empezar con la implementación de funciones matemáticas, una tarea secundaria del elemento de trabajo pendiente prioritario.

En Team Explorer de Visual Studio, en la página Mi trabajo, arrastre esta tarea desde la lista Elementos de trabajo disponibles a la lista Trabajo en curso.

Para revisar el trabajo pendiente y preparar las tareas para iniciar el trabajo

Captura de pantalla de la página Mi trabajo.

  1. En Team Explorer, si aún no está conectado al proyecto en el que quiere trabajar, conéctese al proyecto.

  2. En la página Inicio, seleccione Mi trabajo.

  3. En la página Mi trabajo, arrastre la tarea desde la lista Elementos de trabajo disponibles a la sección Trabajo en curso.

    También puede seleccionar la tarea en la lista Elementos de trabajo disponibles y, a continuación, seleccionar Iniciar.

Borrador de plan de trabajo incremental

El código se desarrolla en una serie de pasos pequeños. Normalmente, cada paso se prolonga no más de una hora y puede llevar solo 10 minutos. En cada paso, escribirá una nueva prueba unitaria y cambiará el código que está desarrollando para que supere la nueva prueba, además de las pruebas que ya ha escrito. A veces, 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. 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.

No insertará el código en el repositorio en Azure DevOps Server hasta que finalice toda la tarea.

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:

  1. Crear el código auxiliar de método, que es, simplemente, la signatura del método.
  2. Satisfacer un caso típico concreto.
  3. Probar un intervalo amplio. Asegurarse de que el código responde correctamente a un intervalo amplio de valores.
  4. Iniciar una excepción si el resultado es negativo. Tratar eficazmente los parámetros incorrectos.
  5. Cobertura de código. Asegurarse de que las pruebas unitarias utilizan al menos el 80 % del código.

Algunos desarrolladores escriben este tipo de plan en los comentarios del código de prueba. Otros simplemente lo memorizan. Puede ser ú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, sabe dónde encontrar la lista cuando pueda volver a ella.

Crear la primera prueba unitaria

Comience creando una prueba unitaria. Comience con la prueba unitaria porque quiere escribir un ejemplo de código que use la 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.

  1. Seleccione Archivo>Nuevo proyecto.
  2. En el cuadro de diálogo Crear un proyecto, seleccione la flecha situada junto a Todos los lenguajes y seleccione C#, seleccione la flecha situada junto a Todos los tipos de proyecto y elija Probar y, a continuación, seleccione Proyecto de prueba de MSTest.
  3. Seleccione Siguiente y, después, Crear.

Captura de pantalla de la prueba unitaria seleccionada en el cuadro de diálogo Crear un nuevo proyecto.

En el editor de código, reemplace el contenido de UnitTest1.cs por el código siguiente. 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, en el momento en que escribe el código, quiere que funcione el ejemplo.

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 tutorial 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.

  1. Cree un proyecto de prueba mediante los pasos anteriores. Puede elegir lenguajes como C#, F# y Visual Basic.

  2. 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 información sobre cómo escribir pruebas unitarias en C++, consulte Escritura de pruebas unitarias para C/C++ con el marco de pruebas unitarias de Microsoft para C++.

Crear código auxiliar para el nuevo código

A continuación, cree un proyecto de biblioteca de clases para el nuevo código. Ahora hay un proyecto para el código en desarrollo y otro proyecto para las pruebas unitarias. Agregue una referencia de proyecto desde el proyecto de prueba al código en desarrollo.

Captura de pantalla del Explorador de soluciones con proyectos de prueba y clase.

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

  1. Coloque el cursor en un ejemplo de la clase que quiere generar, por ejemplo, LocalMath y seleccione Acciones rápidas y refactorizaciones.
  2. En el menú contextual, elija Generar nuevo tipo.
  3. En el cuadro de diálogo Generar tipo, establezca Proyecto en el proyecto de biblioteca de clases. En este ejemplo, es Fabrikam.Math.

Para generar un método

  1. Coloque el cursor en una llamada al método, por ejemplo, SquareRoot, y seleccione Acciones rápidas y refactorizaciones.
  2. En el menú contextual, elija Generar método "SquareRoot".

Ejecutar la primera prueba

Compile y ejecute la prueba. El resultado de la prueba muestra un indicador en rojo de Error y la prueba aparece en la lista de Pruebas no superadas.

Captura de pantalla del Explorador de pruebas que muestra una prueba no superada.

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.

Captura de pantalla del Explorador de pruebas unitarias con una prueba superada.

Para ejecutar pruebas unitarias

Para ejecutar pruebas unitarias:

  • Seleccione Prueba>Ejecutar todas las pruebas
  • O bien, si el Explorador de pruebas está abierto, elija Ejecutar o Ejecutar todas las pruebas en la vista.

Captura de pantalla del Explorador de pruebas que muestra el botón Ejecutar todas.

Si aparece una prueba en Pruebas no superadas, abra la prueba, por ejemplo, haciendo doble clic en el nombre. El punto en el que se ha producido un error en la prueba se muestra en el editor de código.

  • Para ver una lista completa de pruebas, elija Mostrar todo.

  • Para ver los detalles de un resultado de pruebas, seleccione la prueba en el Explorador de pruebas.

  • Para navegar hasta el 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 menú contextual de una o varias pruebas y elija Depurar.

  • Para ejecutar pruebas en segundo plano cada vez que compile la solución, seleccione la flecha situada junto al icono Configuración y, a continuación, seleccione Ejecutar pruebas después de compilar. Las pruebas que no se superaron anteriormente se ejecutan primero.

Aceptación en la interfaz

Puede colaborar con compañeros que usarán el componente compartiendo la pantalla. Un compañero podría comentar que muchas funciones superarían la prueba anterior. Explique que esta prueba era simplemente para asegurarse de que el nombre y los parámetros de la función son correctos y ahora puede escribir una prueba que capture el requisito principal de esta función.

Colabora con compañeros para escribir la prueba siguiente:

[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, 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, esta práctica no es realista, así que escribe las pruebas después de escribir el código. Pero es muy importante escribir pruebas unitarias, ya sea antes o después del código, porque estas mantienen el código estable.

Rojo, verde, refactorizar...

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

Ejecute todas las pruebas, incluida la nueva prueba que ha creado. Después de escribir cualquier prueba, siempre la ejecuta para asegurarse de que no se puede superar antes de escribir el código que permite superarla. Por ejemplo, si olvida colocar aserciones en algunas pruebas que ha escrito, ver el resultado de No superada le da la seguridad de que, al superarla, el resultado de la prueba indica 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. Es posible que le preocupe que esta práctica pueda ralentizar la respuesta de Visual Studio, pero esto rara vez sucede.

Captura de pantalla del Explorador de pruebas con una prueba no superada.

Verde

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;
    }

Ejecuta las pruebas de nuevo y se superan todas.

Captura de pantalla del Explorador de pruebas unitarias con dos pruebas superadas.

Refactorización

Ahora que el código realiza su función principal, lo examina para buscar el modo de que funcione mejor o para facilitar los posibles cambios en el futuro. 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.

Sugerencias

  • 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

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 hace que esté más seguro 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.

Captura de pantalla del Explorador de pruebas con tres pruebas superadas.

Solo para asegurarse de que este resultado no es un error, puede introducir temporalmente un pequeño error en la prueba para que no se supere. Después de ver el error, puede corregirlo de nuevo.

Sugerencia

Haga siempre que una prueba dé error antes de aprobarla.

Excepciones

Ahora, pase 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.

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, puede agregar 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.

Actualice 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:

Captura de pantalla de pruebas unitarias no superadas que se superaron anteriormente.

Busque y corrija el error:

public double SquareRoot(double x)
{
    if (x < 0.0)  // not <=
    {
        throw new ArgumentOutOfRangeException();
    }

Una vez corregido, se superan todas las pruebas:

Captura de pantalla del Explorador de pruebas unitarias con cuatro pruebas superadas.

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 el trabajo y, por último, antes de insertar el código en el repositorio, obtiene un Informe de cobertura de código. Muestra la cantidad de código ejecutado en las pruebas.

El equipo 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ú Prueba de Visual Studio, seleccione Analizar cobertura de código para todas las pruebas. Todas las pruebas se vuelven a ejecutar.

Captura de pantalla del resultado de cobertura de código y el botón Mostrar color.

Al expandir el total en el informe, este muestra que el código que está desarrollando tiene cobertura completa. 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, puede ver qué partes del código de prueba no se han utilizado. El código que no se usó en las pruebas se resalta en naranja. Sin embargo, 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 terminará?

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 inserción en el repositorio validada, 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. Esto permite a los desarrolladores 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, consulte Uso de 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.

  • Las pruebas unitarias simulan todos los aspectos del comportamiento que se requiere, incluidas las entradas típicas y las excepcionales.

  • El código es fácil de entender y de extender.

Cuando se cumplan todos estos criterios, estará preparado para insertar el código en el control de código fuente.

Principios de desarrollo de código con pruebas unitarias

Aplique los siguientes principios al desarrollar código:

  • 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.
  • Insertar en el repositorio 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, vuelva a compartir la pantalla con compañeros para que puedan revisar de forma informal e interactiva lo que ha creado. Las pruebas siguen siendo el centro de la discusión con compañeros que están interesados principalmente en lo que hace el código, no en cómo funciona. Estos compañeros deben estar de acuerdo en que lo que ha escrito satisface sus necesidades.

Compruebe todos los cambios realizados, incluidas las pruebas y el código, y asócielos a las tareas que ha completado. El proceso de inserción en el repositorio 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.

Se le notificará cuando se complete la compilación. En la ventana de resultados de la compilación, observe que la compilación se realizó correctamente y que todas las pruebas se aprobaron.

Para proteger los cambios

  1. En la página Mi trabajo de Team Explorer, seleccione Insertar en el repositorio.

    Captura de pantalla de la inserción en el repositorio desde Mi trabajo.

  2. En la página Cambios pendientes, asegúrese de que:

    • Todos los cambios pertinentes aparecen en Cambios incluidos.
    • Todos los elementos de trabajo relevantes aparecen en Elementos de trabajo relacionados.
  3. Escriba 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.

  4. Elija Insertar en el repositorio.

    Captura de pantalla de la inserción en el repositorio de los cambios pendientes.

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 Configuración de 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.

Captura de pantalla de la página Mis compilaciones con una compilación completada con éxito.

Para obtener más información, consulte Ejecución, supervisión y administración de compilaciones.

Pasos siguientes