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

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

Visual Studio 2019 | Visual Studio 2022

本教程逐步讲解你和你的团队如何最充分地利用最新版本的 Team Foundation 版本控制 (TFVC) 和 Visual Studio 来生成应用。 本教程提供了许多示例,演示如何使用 Visual Studio 和 TFVC 来签出和更新代码、中断时挂起操作、请求代码评审、签入你的更改,以及执行其他任务。

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

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

Visual Studio 的“我的工作”和“代码评审”工具可帮助开发人员管理其工作并与同事协作。

注意

以下版本的 Visual Studio 提供“我的工作”和“代码评审”功能:

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

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

团队同意在当前冲刺 (sprint) 期间,你将处理产品积压工作 (backlog) 中最高优先级的“评估发票状态”工作。 你决定从实现数学函数开始,它是最高优先级的积压工作项的一个子任务。

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

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

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

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

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

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

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

起草增量工作计划

你通过一系列小步骤开发代码。 每个步骤通常不长于 1 小时,可能仅花费十分钟。 在每个步骤中,你编写新的单元测试并更改你开发的代码以便它可以通过新的测试,以及你已写好的测试。 有时你在更改代码之前编写新的测试,有时你在编写测试之前更改代码。 有时你会重构。 也就是说,你只是改进代码,而不添加新测试。 你从不更改通过的测试,除非你认为它没有正确地表示需求。

在每一小步的末尾,你运行所有与该代码区域相关的单元测试。 你认为只有每个测试都通过后,步骤才是完整的。

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

你可以为这一系列的小步骤拟定一个大致的计划。 你知道后面部分的具体细节和排序可能会在工作过程中变化。 下面是最初针对此次特定任务拟定的步骤列表:

  1. 创建测试方法存根,即该方法的签名。
  2. 满足一个特定的典型用例。
  3. 广范围测试。 确保该代码正确地响应各种值。
  4. 在负值时引发异常。 巧妙处理不正确的参数。
  5. 代码覆盖率。 确保单元测试至少执行过 80% 的代码。

一些开发人员在测试代码中编写这种注释型计划。 其他人只是记住他们的计划。 在“任务”工作项的“说明”字段中写下步骤列表可能很有用。 如果你不得不暂时切换到一个更加急迫的任务,那么在可以返回到原来的任务时,你就能知道在何处找到该列表。

创建第一个单元测试

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

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

  1. 选择“文件”>“新建项目” 。
  2. 在“创建新项目”对话框中,选择“所有语言”旁边的箭头并选择“C#”,选择“所有项目类型”旁边的箭头并选择“测试”,然后选择“MSTest 测试项目”。
  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. 在“生成类型”对话框中,将“项目”设置为类库项目。 在此示例中,设为 Fabrikam.Math。

生成方法

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

运行第一个测试

生成并运行测试。 测试结果显示一个红色的“失败”指标,并且测试显示在“失败的测试”列表下。

显示 1 个失败测试的测试资源管理器的屏幕截图。

对代码进行简单更改:

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

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

具有 1 个已通过测试的单元测试资源管理器的屏幕截图。

运行单元测试

若要运行单元测试:

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

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

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

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

  • 若要查看测试结果的细节,请在“测试资源管理器”中选择该测试。

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

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

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

就接口达成一致

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

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

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

提示

针对此函数,你使用测试先行的开发,即首先编写功能的单元测试,然后编写满足测试的代码。 在其他情况下,这种做法并不现实,因此你在编写代码之后再编写测试。 但是编写单元测试非常重要(不管是在编码之前还是之后),因为它们可以保证代码的稳定性。

红色,绿色,重构...

你重复地编写测试并确认失败,编写代码使测试通过,然后考虑重构(即改进代码,而不更改测试),按此顺序循环往复。

Red

运行所有测试,包括你创建的新测试。 你编写任何测试之后,都会运行它以确保失败,然后编写代码使之通过。 例如,如果你忘记在编写的某些测试中放置断言,那么看到“失败”结果会让你确信,你在使其通过时,测试结果正确表明已满足要求。

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

具有 1 个失败测试的测试资源管理器的屏幕截图。

绿色

对目前开发的方法代码进行首次尝试:

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

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

具有 2 个已通过测试的单元测试资源管理器的屏幕截图。

重构

目前代码执行其主函数,请查看代码查找使其性能更佳的方法,或者使其在以后更便于更改。 你可以减少循环中执行的计算次数:

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

该测试首次运行便通过。

具有 3 个已通过测试的测试资源管理器的屏幕截图。

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

提示

在使它通过之前,请始终使未通过测试。

例外

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

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

回归

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

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

查找并修正错误:

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

修正后,所有测试都通过:

具有 4 个已通过测试的单元测试资源管理器的屏幕截图。

提示

每次更改代码后都要确保每个测试均通过。

代码覆盖率

在你工作的时间间隔内,最后签入代码前,获取代码覆盖率报表。 其中显示你的测试执行过多少代码。

你的团队的目标是覆盖率至少为 80%。 因为实现此类代码的高覆盖率可能很困难,所以他们放宽了对生成代码的需求。

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

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

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

当你在报表中展开总计时,它显示你开发的代码具有 100% 的覆盖率。 这是非常令人满意的,因为重要的分数是检验所测试的代码的, 未覆盖的部分其实是测试本身。

通过点击“显示代码覆盖率着色”按钮,你可以查看没有执行的测试代码部分。 测试中未使用的代码以橙色突出显示。 但是,这些部分对覆盖率来说并不重要,因为它们在测试代码中,并且只有在检测到错误时才会用到。

若要验证特定的测试触及到代码的特定分支,你可以设置“显示代码覆盖率着色”,然后使用单个测试的快捷菜单上的“运行”命令来运行该测试。

你什么时候完成?

你继续按小步骤更新代码,直到你对以下项感到满意:

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

    在单元测试集非常大的项目中,让开发人员等待其全部运行是不切实际的。 因而项目会运行一个封闭签入服务,在每个签入搁置集合并到源树之前为其运行所有自动测试。 如果运行失败,则拒绝签入。 这使得开发人员可以在自己的计算机上运行最小的一组单元测试,然后继续执行其他工作,而不会面临中断生成过程的风险。 有关详细信息,请参阅使用封闭签入生成过程以验证更改

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

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

  • 你的代码易于理解和扩展。

当所有这些条件都满足时,你准备将你的代码签入源代码管理中。

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

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

  • 在开发过程中,同时开发单元测试和代码,且经常运行它们。 单元测试可呈现你的组件的规范。
  • 不要更改单元测试,除非需求已更改或测试是错误的。 在扩展代码的功能时逐渐添加新测试。
  • 目标是测试至少覆盖 75% 的代码。 在签入源代码之前,定期查看代码覆盖率结果。
  • 将单元测试与代码一起签入,以便由连续或普通服务器生成运行。
  • 如有可能,为功能的每个部分首先编写单元测试。 执行此操作之后再开发满足它的代码。

签入更改

在签入更改之前,再次与同事共享你的屏幕,以便他们可以非正式地以交互方式与你一起查看你创建的内容。 测试仍是你与同事讨论的焦点,他们感兴趣的主要是代码的功能,而不是它的工作原理。 这些同事应赞同你编写的内容符合他们的需求。

签入所做的所有更改,包括测试和代码,并将其与已完成的任务相关联。 签入操作使用团队的 CI 编译生成过程对团队的自动化团队生成系统进行排序来验证你的更改。 此生成过程在独立于开发计算机的干净环境中编译和测试团队做出的每个更改,帮助团队将代码库中的错误减到最少。

生成结束时,你会收到通知。 在生成结果窗口中,你会看到生成成功,并且所有测试通过。

签入更改

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

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

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

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

  4. 选择“签入”。

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

持续集成代码

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

包含一个成功生成的“我的生成”页的屏幕截图。

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

后续步骤