devops 开发人员的一生中的一天:为用户情景编写新代码

Azure DevOps Services |Azure DevOps Server |Azure DevOps Server 2022 |Azure DevOps Server 2020

Visual Studio 2019 |Visual Studio 2022

本教程将指导你和你的团队如何从最新版本的 Team Foundation 版本控制(TFVC)和 Visual Studio 中获得最大好处,以生成应用。 本教程提供了如何使用 Visual Studio 和 TFVC 签出和更新代码、中断时挂起工作、请求代码评审、签入更改以及执行其他任务的示例。

当团队采用 Visual Studio 和 TFVC 来管理其代码时,他们设置其服务器和客户端计算机、创建积压工作、计划迭代以及完成开始开发应用所需的其他计划。

开发人员查看积压工作,选择要处理的任务。 他们为计划开发的代码编写单元测试。 通常,他们在一小时内多次运行测试,逐渐编写更详细的测试,然后编写使测试通过的代码。 开发人员通常会与将使用其编写的方法的同事讨论其代码接口。

Visual Studio My WorkCode Review 工具可帮助开发人员管理其工作并与同事协作。

注释

Visual Studio My WorkCode Review 功能适用于以下版本:

  • Visual Studio 2022:Visual Studio Community、Visual Studio Professional 和 Visual Studio Enterprise
  • Visual Studio 2019:Visual Studio Professional 和 Visual Studio Enterprise

查看工作项并准备开始工作

团队已同意,在当前冲刺期间,你将处理“ 评估发票”状态,这是产品积压工作中的首要项目。 你决定从 实现数学函数开始,这是首要积压工作项的子任务。

在 Visual Studio 团队资源管理器“我的工作 ”页上,将此任务从 “可用工作项 ”列表拖动到“ 正在进行工作” 列表中。

查看积压工作并准备开始工作的任务

“我的工作”页的屏幕截图。

  1. 团队资源管理器中,如果尚未连接到要处理的项目, 请连接到项目

  2. 主页 中,选择 “我的工作”。

  3. 在“ 我的工作 ”页上,将任务从 “可用工作项 ”列表拖到“ 正在进行工时 ”部分。

    还可以在 “可用工作项 ”列表中选择任务,然后选择“ 开始”。

起草增量工作计划

在一系列小步骤中开发代码。 每个步骤通常需要不超过一小时,可能需要 10 分钟的时间。 在每个步骤中,除了已编写的测试之外,你还编写了一个新的单元测试并更改正在开发的代码,以便它通过新测试。 有时在更改代码之前编写新测试,有时在编写测试之前更改代码。 有时重构。 也就是说,只需在不添加新测试的情况下改进代码。 你永远不会更改通过的测试,除非你决定它未正确表示要求。

在每个小步骤结束时,运行与此代码区域相关的所有单元测试。 在每次测试通过之前,你不会考虑完成步骤。

在完成整个任务之前,不会将代码签入到 Azure DevOps Server。

可以记下此小步骤序列的粗略计划。 你知道以后的确切详细信息和顺序可能会在工作时更改。 下面是此特定任务的初始步骤列表:

  1. 创建测试方法存根,即只是方法的签名。
  2. 满足一个特定的典型案例。
  3. 测试范围广泛。 确保代码正确响应大量值。
  4. 负数异常。 使用不正确的参数正常处理。
  5. 代码覆盖率。 确保单元测试至少执行代码的 80%。

一些开发人员在测试代码中的注释中编写此类计划。 其他人只是记住他们的计划。 在“任务工作项 的说明 ”字段中编写步骤列表非常有用。 如果必须暂时切换到更紧迫的任务,当能够返回列表时,你知道在何处查找列表。

创建第一个单元测试

首先创建单元测试。 从单元测试开始,因为你想要编写使用新类的代码示例。

这是要测试的类库的第一个单元测试,因此创建新的单元测试项目。

  1. 选择文件>新建项目
  2. 在“创建新项目”对话框中,选择“所有语言”旁边的箭头,然后选择“C#”,选择“所有项目类型”旁边的箭头,然后选择测试项目”。
  3. 选择 “下一步”,然后选择“ 创建”。

“创建新项目”对话框中所选的“单元测试”的屏幕截图。

在代码编辑器中,将 UnitTest1.cs 的内容替换为以下代码。 在此阶段,你只想说明将如何调用其中一个新方法:

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

在测试方法中编写示例是因为,在编写代码时,希望该示例正常工作。

创建单元测试项目和方法

通常,为要测试的每个项目创建新的测试项目。 如果测试项目已存在,则只需添加新的测试方法和类。

本教程使用 Visual Studio 单元测试框架,但也可以使用来自其他提供程序的框架。 如果安装适当的适配器,测试资源管理器与其他框架同样有效。

  1. 使用前面的步骤创建测试项目。 可以选择 C#F#Visual Basic 等语言。

  2. 将测试添加到提供的测试类。 每个单元测试都是一种方法。

    • 每个单元测试都必须以 TestMethod 特性为前缀,单元测试方法不应具有任何参数。 可以对单元测试方法使用所需的任何名称:

      [TestMethod]
      public void SignatureTest()
      {...}
      
      <TestMethod()>
      Public Sub SignatureTest()
      ...
      End Sub
      
    • 每个测试方法都应调用类的方法 Assert ,以指示它是否已通过还是失败。 通常,验证作的预期结果和实际结果是否相等:

      Assert.AreEqual(expectedResult, actualResult);
      
      Assert.AreEqual(expectedResult, actualResult)
      
    • 测试方法可以调用没有 TestMethod 该属性的其他普通方法。

    • 可以将测试组织到多个类中。 每个类都必须以 TestClass 特性为前缀。

      [TestClass]
      public class UnitTest1
      { ... }
      
      <TestClass()>
      Public Class UnitTest1
      ...
      End Class
      

有关如何在 C++ 中编写单元测试的信息,请参阅 使用适用于 C++ 的 Microsoft 单元测试框架编写 C/C++单元测试

为新代码创建存根

接下来,为新代码创建类库项目。 现在有一个项目用于正在开发的代码,还有一个项目用于单元测试。 将测试项目中的项目引用添加到开发中的代码。

包含测试和类项目的解决方案资源管理器的屏幕截图。

在新项目中,添加新类和最少版本的方法,至少允许测试成功生成。 执行此作的最快方法是从测试中的调用生成类和方法存根。

public double SquareRoot(double p)
{
    throw new NotImplementedException();
}

从测试生成类和方法

首先,创建要在其中添加新类的项目,除非它已存在。

生成类

  1. 将光标置于要生成的类示例上,例如, LocalMath然后选择 “快速作”和“重构”。
  2. 在快捷菜单上,选择“ 生成新类型”。
  3. 在“ 生成类型 ”对话框中,将 Project 设置为类库项目。 在此示例中,它是 Fabrikam.Math

生成方法

  1. 例如,将光标置于对方法的调用上, SquareRoot然后选择 “快速作”和“重构”。
  2. 在快捷菜单上,选择 “Generate 方法”SquareRoot”。

运行第一个测试

生成并运行测试。 测试结果显示红色 的“失败 ”指示器,测试显示在 “失败的测试”列表中。

测试资源管理器的屏幕截图,其中显示了一个失败的测试。

对代码进行简单的更改:

public double SquareRoot(double p)
{
    return 0.0;
}

再次运行测试,然后通过。

单元测试资源管理器的屏幕截图,其中包含一个通过的测试。

运行单元测试

运行单元测试:

  • 选择“测试>运行所有测试
  • 或者,如果 测试资源管理器 处于打开状态,请选择 “运行 ”或 “在视图中运行所有测试”。

“测试资源管理器”的屏幕截图,其中显示了“全部运行”按钮。

如果测试出现在 “失败的测试”下,请通过双击名称打开测试。 测试失败的点显示在代码编辑器中。

  • 若要查看测试的完整列表,请选择“ 全部显示”。

  • 若要查看测试结果的详细信息,请在 测试资源管理器中选择测试

  • 若要导航到测试的代码,请在 测试资源管理器中双击测试,或选择快捷菜单上的 “打开测试 ”。

  • 若要调试测试,请打开一个或多个测试的快捷菜单,然后选择 “调试”。

  • 若要在生成解决方案时在后台运行测试,请选择 “设置” 图标旁边的箭头,然后选择“ 生成后运行测试”。 先运行以前失败的测试。

就接口达成一致

可以通过共享屏幕与将使用组件的同事协作。 同事可能会评论说,很多函数都会通过上一次测试。 说明此测试只是为了确保函数的名称和参数正确,现在你可以编写一个可捕获此函数的主要要求的测试。

与同事协作编写以下测试:

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

小窍门

对于此函数,请使用 测试第一个开发,在该开发中首先编写功能的单元测试,然后编写满足测试的代码。 在其他情况下,这种做法并不现实,因此在编写代码后编写测试。 但是,无论是代码之前还是之后,编写单元测试非常重要,因为它们使代码保持稳定。

红色、绿色、重构...

按照循环作,在该循环中反复编写测试并确认测试失败、编写代码以使测试通过,然后考虑重构,即在不更改测试的情况下改进代码。

红色

运行所有测试,包括你创建的新测试。 编写任何测试后,请始终运行它,以确保在编写使测试通过的代码之前失败。 例如,如果忘记在编写的某些测试中放置断言,看到 “失败 ”结果可让你确信在通过时,测试结果正确指示已满足要求。

另一个有用的做法是在 生成后设置运行测试。 每次生成解决方案时,此选项都会在后台运行测试,以便持续报告代码的测试状态。 你可能担心这种做法可能会使 Visual Studio 响应速度缓慢,但这很少发生。

测试资源管理器的屏幕截图,其中一个测试失败。

Green

在要开发的方法的代码中编写第一次尝试:

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

再次运行测试,所有测试都通过。

单元测试资源管理器的屏幕截图,其中包含两个通过的测试。

重构

现在,代码执行其主函数,请查看代码以查找提高其性能的方法,或使将来更易于更改。 可以减少循环中执行的计算数:

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

验证测试是否仍通过。

提示

  • 开发代码时所做的每一项更改都应是重构或扩展:

    • 重构意味着不会更改测试,因为你没有添加新功能。
    • 扩展意味着添加测试和进行代码更改,这些更改是传递现有测试和新测试所必需的。
  • 如果要将现有代码更新为已更改的要求,则还会删除不再表示当前要求的旧测试。

  • 避免更改已通过的测试。 而是添加新的测试。 仅编写表示实际要求的测试。

  • 每次更改后运行测试。

...并重复

使用小步骤列表作为粗略指南,继续执行一系列扩展和重构步骤。 在每个扩展之后,你并不总是执行重构步骤,有时连续执行多个重构步骤。 但是,每次更改代码后,始终运行单元测试。

有时,你添加的测试不需要对代码进行任何更改,但这会增加你对代码正常运行的信心。 例如,你希望确保函数在广泛的输入范围内工作。 编写更多测试,如以下测试:

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

此测试首次运行时通过。

测试资源管理器的屏幕截图,其中包含三个通过的测试。

为了确保此结果不是错误,可以暂时在测试中引入一个小错误,使其失败。 看到失败后,可以再次修复它。

小窍门

在通过测试之前,请始终使测试失败。

例外

现在,继续编写针对异常输入的测试:

[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");
}

此测试将代码放入循环中。 必须在测试资源管理器中使用“取消”按钮。 这将在 10 秒内终止代码。

你希望确保生成服务器上无法发生无休止的循环。 尽管服务器对完整运行施加了超时,但超时时间非常长,并且会导致大量延迟。 因此,可以向此测试添加显式超时:

[TestMethod, Timeout(1000)]
public void RootTestNegativeInput()
{...

显式超时使测试失败。

更新代码以处理此异常情况:

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

Regression

新的测试通过,但存在回归。 用于通过的测试现在失败:

单元测试失败的屏幕截图,之前通过。

查找并修复错误:

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

修复后,所有测试都会通过:

单元测试资源管理器的屏幕截图,其中包含四个通过的测试。

小窍门

请确保每次更改代码后,每个测试都会通过。

代码覆盖率

在工作时的时间间隔内,最后在签入代码之前,获取代码覆盖率报告。 这显示了测试已经执行了多少代码。

你的团队的目标是覆盖至少 80%。 它们放宽了对生成的代码的这一要求,因为很难实现此类代码的高覆盖率。

良好的覆盖率不能保证组件的完整功能已经过测试,也不保证代码适用于每个输入值范围。 然而,代码行覆盖率和组件行为空间覆盖率之间存在相当密切的相关性。 因此,良好的覆盖范围增强了团队的信心,即他们正在测试他们应该采取的大部分行为。

若要获取代码覆盖率报告,请在 Visual Studio 测试 菜单中,选择“ 分析所有测试的代码覆盖率”。 所有测试都会再次运行。

代码覆盖率结果和“显示颜色”按钮的屏幕截图。

在报表中展开总计时,它显示正在开发的代码具有完整的覆盖范围。 这是非常令人满意的,因为重要的分数是测试中的代码。 发现的部分实际上是在测试中。

通过切换 “显示代码覆盖率着色 ”按钮,可以看到测试代码的哪些部分尚未练习。 测试中未使用的代码以橙色突出显示。 但是,这些部分对于覆盖率来说并不重要,因为它们位于测试代码中,并且仅在检测到错误时才使用。

若要验证特定测试是否进入代码的特定分支,可以设置 “显示代码覆盖率着色 ”,然后使用其快捷菜单上的 “运行” 命令运行单个测试。

何时完成?

继续按小步骤更新代码,直到你满意:

  • 所有可用的单元测试都通过。

    在具有大量单元测试的项目中,开发人员可以不切实际地等待它们全部运行。 相反,该项目会运行一个封闭的签入服务,在该服务中,所有自动测试都会针对每个已签入货架集运行,然后再将其合并到源树中。 如果运行失败,则拒绝签入。 这样,开发人员就可以在自己的计算机上运行一组最小的单元测试,然后继续执行其他工作,而不会运行中断生成的风险。 有关详细信息,请参阅 使用封闭的签入生成过程来验证更改

  • 代码覆盖率符合团队的标准。 75% 是典型的项目要求。

  • 单元测试模拟所需的行为的各个方面,包括典型输入和异常输入。

  • 代码易于理解和扩展。

满足所有这些条件后,即可将代码签入源代码管理。

使用单元测试开发代码的原则

在开发代码时应用以下原则:

  • 开发单元测试以及代码,并在开发期间频繁运行。 单元测试表示组件的规范。
  • 请勿更改单元测试,除非要求已更改或测试错误。 扩展代码的功能时,逐步添加新测试。
  • 目标是测试至少涵盖 75 个代码%。 在签入源代码之前,请每隔一段时间查看代码覆盖率结果。
  • 连同代码一起签入单元测试,以便由连续或常规服务器生成运行。
  • 在实际情况下,对于每个功能片段,请先编写单元测试。 在开发满足代码的代码之前执行此作。

签入更改

在签入更改之前,请再次与同事共享你的屏幕,以便他们可以与你创建的内容进行非正式和交互式的评审。 这些测试仍然是你与主要感兴趣的代码用途的同事讨论的重点,而不是代码的工作原理。 这些同事应该同意你编写的满足他们的需求。

签入你所做的所有更改,包括测试和代码,并将其与已完成的任务相关联。 签入会将团队的自动化团队生成系统排入队列,以使用团队的 CI 生成 过程验证更改。 此生成过程通过构建和测试团队在独立于开发计算机的干净环境中生成和测试,帮助团队最大程度地减少代码库中的错误。

生成完成后,会收到通知。 在生成结果窗口中,可以看到生成成功,并且通过了所有测试。

签入更改

  1. 团队资源管理器中的“我的工作”页上,选择“签入”。

    从“我的工作”签入的屏幕截图。

  2. “挂起的更改 ”页上,确保:

    • 所有相关更改都列在 “包含的更改”中。
    • 所有相关工作项都列在 相关工作项中。
  3. 输入 注释 ,以帮助团队了解这些更改的目的,当他们查看已更改文件和文件夹的版本控制历史记录。

  4. 选择 “签入”。

    签入挂起的更改的屏幕截图。

持续集成代码

有关如何定义持续集成生成过程的详细信息,请参阅 设置 CI 生成。 设置此生成过程后,可以选择收到有关团队生成结果的通知。

“我的生成”页的屏幕截图,其中显示了成功生成。

有关详细信息,请参阅 “运行”、“监视和管理生成”。

后续步骤