在.Net Core中测试

2025-06-04

在.Net Core中测试

在Twitter上关注我,很高兴接受您对主题或改进的建议/Chris

测试是我们需要做的,它可以增强我们对所构建产品的信心。毕竟,我们想要交付能够正常工作的软件。我们知道错误难免会发生,所以我们需要严格遵守测试规范,最好为每个错误添加一个测试,这样我们至少知道我们已经修复了那个错误。这就像一场打地鼠的游戏,我们必须保持跟进。然而,测试的方法有很多种,例如单元测试、集成测试和端到端测试。在本文中,我们将重点介绍单元测试,并确保我们采用一些良好的实践。

简而言之:这是一篇关于 .Net Core 测试的入门文章。如果您是 .Net Core 测试新手,或者完全不懂 .Net Core 测试,那么这篇文章非常适合您。之后我会写一篇更高级的测试文章,讨论对象母体、模拟测试以及其他相关内容。

在本文中,我们将介绍

  • 创建测试项目并运行测试
  • 不同类型的测试,如果您对测试完全陌生并且需要了解不同阶段和级别的入门知识,我们将在这里提到不同类型的测试。
  • 编写测试,这里我们将介绍一些命名和安排测试的良好做法。

.Net Core 支持使用以及进行测试MsTest因此您有很多选择。在本文中,我们选择了。查看参考部分以获取其他框架的链接。xUnitnUnitMsTest

参考

为什么

我们在文章开头提到,测试非常重要,尤其是在发现 bug 的情况下。我认为,测试是开发者工具箱中最重要的工具。学会检查你的工作不仅能为你在客户和同事中树立良好的声誉。你越擅长测试代码,并找到各种测试方法,对每个人都越有利。

不同类型的测试

测试有多种类型,用于软件生命周期的不同阶段。您还可以编写不同级别的测试来测试细节或大规模行为。我们通常说单元测试用于测试实现细节,而集成测试则用于测试两个或多个更大的组件是否协同工作。我们应该在各个级别和生命周期进行测试。

最后测试

我们已经完成了整个软件的编写,所谓的“快乐路径”似乎行得通。快乐路径指的是我们认为客户会用来执行任务的场景,或者运行我们软件的其他程序,也可能是其他软件 :)。在这种情况下,我们意识到如果发生意外情况,我们可能会遇到麻烦,所以我们开始在此时添加测试并记录日志,以确保我们的软件涵盖了我们认为可能发生的一些场景。在这个时候添加测试通常感觉很麻烦,坦白说,一点也不好玩。但我们必须添加它们,才能称自己是专业人士。

回归测试

嗯,我想说测试应该一直伴随着你,要时刻思考这个问题:它会不会出问题?是输入的类型问题,是内存消耗问题,还是存在一个依赖关系,它会以我们意想不到的方式给出答案?

当然,你不可能事事都考虑到,即使你尽力尝试,这种在代码编写之前、编写过程中和编写之后的深思熟虑的分析也肯定会让代码变得更好。正因为你无法事事都考虑到,所以难免会有bug。一个好的做法是尝试编写一个测试来证明bug的存在。一旦确定了,你就可以继续修复bug,并确保测试通过。至少你已经解决了那个症状/bug。

以上并非真正的回归测试,而是在发现 bug 时的一种良好实践。回归测试更多是指在进行变更(例如添加功能或修复 bug)时,重新运行一些测试以确保一切正常。回归测试与代码紧密相关,你应该了解这个术语。

重构

当然,重构还有另一种情况。我见过的大多数代码库随着时间的推移,随着越来越多的功能被添加进来,或者功能本身发生变化,变得杂乱无章。曾经易于理解的代码变成了一团乱麻。这时,你很可能想从头开始。进行测试是一个很好的方法,可以让你确信即使你修改了代码,即使你停止通过重构改进,它仍然能够正常工作。

测试驱动开发测试优先

这是一种方法论,在实际开始编写任何生产代码之前,你需要构思并编写测试。其理念是,先从失败的测试开始,然后编写生产代码,直到测试通过。这被称为红绿测试。红色表示测试失败,绿色表示测试通过。有些人喜欢这种工作方式,而另一些人则觉得它有些限制。积极的一面是,你不会编写不必要的代码,因为你编写的所有代码都是为了通过特定的测试场景。

集成测试

这通常是一些高级测试,我们测试不同组件(系统的主要组成部分)是否能够协同工作。通常是某一层与另一层通信,例如 API与数据层通信,甚至是一个单独的组件与另一个组件通信,这些组件共同构成了一个庞大而复杂的系统。

什么 - 创建演示

在本文中,我们的目标是在较低的级别(即单元测试级别)进行测试。我们将展示如何轻松创建测试项目、编写测试并运行它们。希望我们也能提供一些关于编写测试以及如何改进测试的指导。我们将进行以下操作:

  1. 搭建测试项目
  2. 创作测试
  3. 运行测试
  4. 改进我们的测试

搭建测试项目

本质上,我们希望创建一个解决方案,其中应用程序代码作为一个项目,测试代码则放在另一个单独的项目中。因此,整体解决方案如下:

solution
  app // project containing our implementation
  app-test // project containing our tests
Enter fullscreen mode Exit fullscreen mode

创建解决方案

我们先创建一个目录。这里面存放着我们的解决方案。这个目录可以随意命名,但我们的命名方式TestExample如下:

mkdir TestExample
Enter fullscreen mode Exit fullscreen mode

接下来我们将通过调用以下命令来创建解决方案:

cd TestExample
dotnet new sln
Enter fullscreen mode Exit fullscreen mode

创建库项目

此后,我们将创建一个库项目来保存我们的生产代码:

dotnet new classlib -o app
Enter fullscreen mode Exit fullscreen mode

接下来我们要将项目引用添加到解决方案中,如下所示:

dotnet sln add app/app.csproj
Enter fullscreen mode Exit fullscreen mode

创建测试项目

此后,我们要创建一个如下测试项目:

dotnet new mstest -o app-test
Enter fullscreen mode Exit fullscreen mode

它应该产生类似这样的输出:

并添加对我们的解决方案的引用,如下所示:

dotnet sln add app-test/app-test.csproj
Enter fullscreen mode Exit fullscreen mode

app最后,我们需要在项目中添加对项目的引用,app-test以便引用我们要测试的应用程序中的代码。为此,请输入以下命令:

cd app-test
dotnet add reference ../app/app.csproj
cd ..
Enter fullscreen mode Exit fullscreen mode

编写测试

让我们创建一个文件CalculatorTest.cs并赋予它以下内容:

// CalculatorTest.cs

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace app_test 
{
  [TestClass]
  public class CalculatorTest 
  {
    [TestMethod]
    public void Add() 
    {
      Assert.AreEqual(1,1);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

装饰器TestClass意味着我们的类CalculatorTest应该被视为测试类。TestMethod我们添加到方法中的装饰器是Add(),表示这是一个测试方法。
在 方法 bofy 中,Add()我们添加了一个断言语句Assert.AreEqual(1,1)。 的原型AreEqual()AreEqual(expected, actual)。这就是我们创建测试所需的全部内容。

运行测试

现在,让我们运行它。我们可以通过以下方式之一来执行:

  1. 终端命令

此命令dotnet test带有许多参数,因此请务必查看文档页面

  1. Run Test在 VS Code 中单击

正如您在上面看到的,我们也可以轻松地调试测试,如果我们查看类级别,将会有一个链接来调试/运行类中的所有测试。

我们有一个叫做 的东西filter,可以用来运行我们针对的特定测试。让我们添加另一个测试,如下所示:

[TestMethod]
[Priority(1)]
public void Subtract() 
{
  Assert.AreEqual(1,2);
}
Enter fullscreen mode Exit fullscreen mode

上面我们不仅添加了测试方法Subtract(),还添加了装饰器Priority(1)。我们可以通过输入以下内容进行过滤,以便只运行与此名称匹配的测试:

dotnet test --filter Priority=1
Enter fullscreen mode Exit fullscreen mode

正如您在上面看到的,它只运行1测试Subtract(),它跳过了我们的Add()测试。

我们可以添加另一个装饰器TestCategory。让我们添加以下两个测试:

[TestMethod]
[TestCategory("DivideAndMultiply")]
public void Divide() 
{
  Assert.AreEqual(1,1);
}

[TestMethod]
[TestCategory("DivideAndMultiply")]
public void Multiply()
{
  Assert.AreEqual(1, 1);
}
Enter fullscreen mode Exit fullscreen mode

这两个测试具有相同的标签DivideAndMultiply。我们可以通过输入 来过滤它dotnet test --filter TestCategory=DivideAndMultiply。这TestCategory使我们能够创建比 更具描述性的标签Priority

更真实的场景

好的,我们有一个测试类,CalculatorTest但说实话,在生产环境中我们有多少次编写过计算器代码?是的,我没想到。我们很可能在做类似电商公司的事情。所以,让我们想象一下它是什么样子。我们来谈谈订单以及如何评估它们。

这很简单,我收到一份订单和一些商品,然后循环遍历它们,得到总数,然后添加一些运费,然后我就完成了:)

我希望它真的那么简单。刚开始的时候或许可以,但后来有人提到“折扣”这个词,当然,20% 的折扣很容易计算,但突然间它就变成了一个庞然大物。不知不觉中,你就得到了不同产品类型的折扣,买二送一的折扣,节日折扣,所有以特定方式标记的商品的折扣等等。相信我,折扣很快就会变成一个失控的庞然大物,需要一个团队来管理,你可以想象为了跟上这种局面,代码会是什么样子。

开始

让我们从小事做起。假设你刚刚启动了一个 WebShop,需要编写一些代码来支持计算订单金额。我们首先在项目Order.cs中创建文件app,并为其添加以下内容:

// Order.cs

using System;
using System.Collections.Generic;

namespace OrderSystem {
  public enum ProductType 
  {
    CD,
    DVD,
    Book,
    Clothes,
    Game
  }

  public class Product
  {
    public string Name { get; set; }
    public string Description { get; set; }
    public ProductType Type { get; set; }
  }

  public class OrderItem
  {
    public int Quantity { get; set; }
    public double Price { get; set; }
    public Product Product { get; set; }
  }

  public class Order 
  {
    public List<OrderItem> Items { get; set; }
    public DateTime Created { get; set; }
  }
}

Enter fullscreen mode Exit fullscreen mode

然后,我们创建一个文件OrderHelper.cs,用于计算订单总额及相关Order逻辑。目前,我们将其内容设置为:


namespace OrderSystem
{
  public class OrderHelper
  {
      public OrderHelper() 
      {}

      public static double Cost(Order order)  
      {
        return 0;
      }
  }
}
Enter fullscreen mode Exit fullscreen mode

好的,这没什么用,这就是重点,因为现在我们要编写一个测试。

让我们OrderHelperTest.cs在项目中创建一个文件app-test并赋予它以下内容:

using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OrderSystem;

namespace app_test
{
  [TestClass]
  public class OrderHelperTest
  {
      [TestMethod]
      public void ShouldSumCostOfOrder()
      {
          // Arrange

          // Act

          // Assert
      }
  }
}

Enter fullscreen mode Exit fullscreen mode

创作时的良好做法

现在,我们来谈谈编写测试时的一些最佳实践。测试应该易于阅读。测试名称应该能够说明我们正在做什么以及我们希望得到的结果。命名能够ShouldSumCostOfOrder()告诉我们发生了什么。你甚至可以更具体一些,例如描述结果ShouldSumOrderTo30()。最终,这取决于你和你正在测试的内容。

接下来要查看的是三个As,ArrangeActAssert在 中,你应该设置好所有需要的内容。在 中,Act你应该调用你的生产代码。在最后Assert一步,你应该验证你是否得到了预期的结果。

正如您所见,我们有一种方法ShouldSumCost()

编写测试

好了,我们对命名以及测试步骤有了更多的了解,接下来让我们来详细讲解一下测试。首先需要关注的是设置测试,也就是所谓的Arrange阶段,代码如下:

// Arrange
var productCD = new Product()
{
    Type = ProductType.CD,
    Name = "Nirvana",
    Description = "Album"
};
var productMovie = new Product()
{
    Type = ProductType.DVD,
    Name = "Gladiator",
    Description = "Movie"
};

var order = new Order() 
{
    Items = new List<OrderItem>() { 
        new OrderItem(){ 
            Quantity = 1, 
            Price = 10,
            Product = productCD
        },
        new OrderItem(){
            Quantity = 2,
            Price = 10,
            Product = productMovie
        } 
    }
};
Enter fullscreen mode Exit fullscreen mode

我们可以轻松地将它们移到另一个类中,以使测试更易于阅读。不过现在,让我们继续进行下一阶段Act

// Act
var actual = OrderHelper.Cost(order);
Enter fullscreen mode Exit fullscreen mode

注意变量的命名actual。编写测试时,使用像actual和 这样的名称是一种很好的做法expected,因为这些是其他开发人员熟悉的术语,这将使代码更容易阅读。

最后,我们进入Assert检查代码是否按照我们的预期执行的阶段:

// Assert
Assert.AreEqual(30, actual);
Enter fullscreen mode Exit fullscreen mode

完整文件OrderHelperTest.cs应如下所示:

// OrderHelperTest.cs

using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OrderSystem;

namespace app_test
{
  [TestClass]
  public class OrderHelperTest
  {
    [TestMethod]
    public void ShouldSumCostOfOrder()
    {
        // Arrange
        var productCD = new Product()
        {
            Type = ProductType.CD,
            Name = "Nirvana",
            Description = "Album"
        };
        var productMovie = new Product()
        {
            Type = ProductType.DVD,
            Name = "Gladiator",
            Description = "Movie"
        };

        var order = new Order() 
        {
            Items = new List<OrderItem>() { 
                new OrderItem(){ 
                    Quantity = 1, 
                    Price = 10,
                    Product = productCD
                },
                new OrderItem(){
                    Quantity = 2,
                    Price = 10,
                    Product = productMovie
                } 
            }
        };

        // Act
        var actual = OrderHelper.Cost(order);

        // Assert
        Assert.AreEqual(30, actual);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

此时我们的测试将会失败

添加代码

好的,我们需要通过添加一个真正的实现来修复测试OrderHelper.cs。将代码更改为以下内容:

using System.Linq;

namespace OrderSystem
{
  public class OrderHelper
  {
      public static double Cost(Order order)  
      {
          return order.Items.Sum(i => i.Price * i.Quantity);
      }
  }
}
Enter fullscreen mode Exit fullscreen mode

重新运行测试我们得到:

实用功能

好的,我们已经带你了解了所谓的Red-Green测试,首先编写一个测试,确保它失败,然后返回结果Red。然后我们编写实现。最后,我们重新运行测试,如果测试通过,结果返回结果Green

很多时候,我们最终编写的测试都是通过代码来测试相同的流程,我们只需要改变输入以确保我们的生产代码确实有效。

以我们的CalculatorTest.cs文件为例,首先Calculator.csapp项目中给它一个真正的实现。现在给它添加以下内容:

// Calculator.cs

namespace app 
{
  public class Calculator 
  {
    public static int Add(int lhs, int rhs)
    {
      return 0;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

更新我们CalculatorTest.cs现在说:

// CalculatorTest.cs

using app;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace app_test 
{
  [TestClass]
  public class CalculatorTest 
  {
    [TestMethod]
    public void Add() 
    {
      var actual = Calculator.Add(0,0);
      Assert.AreEqual(actual,0);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

此时运行测试意味着测试通过了。那么,我们现在可以相信代码能够正常工作了吗?我的意思是测试通过了,对吧?由于实现代码非常小,我们可以断定,如果更改输入,它将无法正常工作。但如果实现代码规模较大,可能就很难判断了。

让我们尝试找到一种方法!

将您的代码更改为以下内容:

// CalculatorTest.cs

using app;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace app_test 
{
  [TestClass]
  public class CalculatorTest 
  {
    [DataTestMethod]
    [DataRow(0)]
    public void Add(int value) 
    {
      var actual = Calculator.Add(value, value);
      var expected = value + value
      Assert.AreEqual(actual, expected);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

注意我们如何将TestMethod其移除并替换为DataTestMethod。另外,请注意我们如何添加DataRow一个接受参数的 。现在,这个参数成为了输入参数value。现在运行测试意味着它会进行计算0 + 0,因此我们的测试仍然应该通过。让我们添加一些其他场景,使代码如下所示:

using System;
using app;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace app_test 
{
  [TestClass]
  public class CalculatorTest 
  {
    [DataTestMethod]
    [DataRow(0)]
    [DataRow(1)]
    [DataRow(2)]
    public void Add(int value) 
    {
      Console.WriteLine("Testing {0} + {0}", value);
      var actual = Calculator.Add(value, value);
      var expected = value + value;
      Assert.AreEqual(actual,expected);
    } 
  }
}
Enter fullscreen mode Exit fullscreen mode

现在运行测试我们得到:

从上面我们可以看出, 可以0 + 0通过1 + 12 + 2失败了。我们的代码无法运行,所以我们需要修复它。让我们将其更改Calculator.cs为以下内容:

namespace app 
{
  public class Calculator 
  {
    public static int Add(int lhs, int rhs)
    {
      return lhs + rhs;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

现在如果我们运行测试,我们会得到:

我们刚才做的叫做数据驱动测试。文章已经够长了,就此打住吧。

概括

我们已指导您创建测试项目、编写一些测试并学习如何运行测试。此外,我们还讨论了一些命名方面的良好实践,以及如何通过数据驱动测试进一步改进您的测试。

我希望这篇文章对您有所帮助,并且您期待下一篇更深入的文章。

文章来源:https://dev.to/dotnet/testing-in-net-core-ojh
PREV
.NET 团队最喜欢的 Razor 功能 TagHelperPack BlazorWebFormsComponents
NEXT
使用 Lucene.NET 在 Blazor WebAssembly 中实现搜索