单元测试与集成测试

测试必要性说明

相信大家在看到单元测试与集成测试这个标题时,会有很多感慨,我们无数次的在实践中提到要做单元测试、集成测试,但是大多数项目都没有做或者仅建了项目文件。这里有客观原因,已经接近交付日期了,我们没时间做白盒测试了。也有主观原因,面对业务复杂的代码我们不知道如何入手做单元测试,不如就留给黑盒测试吧。但是,当我们的代码无法进行单元测试的时候,往往就是代码开始散发出坏味道的时候。长此以往,将欠下技术债务。在实践过程中,技术债务常常会存在,关键在于何时偿还,如何偿还。

上图说明了随着时间的推移开发/维护难度的变化。

测试框架选择

在 .NET Core 中,提供了 xUnit 、NUnit 、 MSTest 三种单元测试框架。

MSTest UNnit xUnit 说明 提示
[TestMethod] [Test] [Fact] 标记一个测试方法
[TestClass] [TestFixture] n/a 标记一个 Class 为测试类,xUnit 不需要标记特性,它将查找程序集下所有 Public 的类
[ExpectedException] [ExpectedException] Assert.Throws 或者 Record.Exception xUnit 去掉了 ExpectedException 特性,支持 Assert.Throws
[TestInitialize] [SetUp] Constructor 我们认为使用 [SetUp] 通常来说不好。但是,你可以实现一个无参构造器直接替换 [SetUp]。 有时我们会在多个测试方法中用到相同的变量,熟悉重构的我们会提取公共变量,并在构造器中初始化。但是,这里我要强调的是:在测试中,不要提取公共变量,这会破坏每个测试用例的隔离性以及单一职责原则。
[TestCleanup] [TearDown] IDisposable.Dispose 我们认为使用 [TearDown] 通常来说不好。但是你可以实现 IDisposable.Dispose 以替换。 [TearDown] 和 [SetUp] 通常成对出现,在 [SetUp] 中初始化一些变量,则在 [TearDown] 中销毁这些变量。
[ClassInitialize] [TestFixtureSetUp] IClassFixture< T > 共用前置类 这里 IClassFixture< T > 替换了 IUseFixture< T > ,参考
[ClassCleanup] [TestFixtureTearDown] IClassFixture< T > 共用后置类 同上
[Ignore] [Ignore] [Fact(Skip="reason")] 在 [Fact] 特性中设置 Skip 参数以临时跳过测试
[Timeout] [Timeout] [Fact(Timeout=n)] 在 [Fact] 特性中设置一个 Timeout 参数,当允许时间太长时引起测试失败。注意,xUnit 的单位时毫秒。
[DataSource] n/a [Theory], [XxxData] Theory(数据驱动测试),表示执行相同代码,但具有不同输入参数的测试套件 这个特性可以帮助我们少写很多代码。

以上写了 MSTest 、UNnit 、 xUnit 的特性以及比较,可以看出 xUnit 在使用上相对其它两个框架来说提供更多的便利性。但是这里最终实现还是看个人习惯以选择。

单元测试

  1. 新建单元测试项目

  2. 新建 Class

  3. 添加测试方法

            /// <summary>
    /// 添加地址
    /// </summary>
    /// <returns></returns>
    [Fact]
    public async Task Add_Address_ReturnZero()
    {
    DbContextOptions<AddressContext> options = new DbContextOptionsBuilder<AddressContext>().UseInMemoryDatabase("Add_Address_Database").Options;
    var addressContext = new AddressContext(options); var createAddress = new AddressCreateDto
    {
    City = "昆明",
    County = "五华区",
    Province = "云南省"
    };
    var stubAddressRepository = new Mock<IRepository<Domain.Address>>();
    var stubProvinceRepository = new Mock<IRepository<Province>>();
    var addressUnitOfWork = new AddressUnitOfWork<AddressContext>(addressContext); var stubAddressService = new AddressServiceImpl.AddressServiceImpl(stubAddressRepository.Object, stubProvinceRepository.Object, addressUnitOfWork);
    await stubAddressService.CreateAddressAsync(createAddress);
    int addressAmountActual = await addressContext.Addresses.CountAsync();
    Assert.Equal(1, addressAmountActual);
    }
    • 测试方法的名字包含了测试目的、测试场景以及预期行为。
    • UseInMemoryDatabase 指明使用内存数据库。
    • 创建 createAddress 对象。
    • 创建 Stub 。在单元测试中常常会提到几个概念 Stub , Mock 和 Fake ,那么在应用中我们该如何选择呢?
      • Fake - Fake 通常被用于描述 Mock 或 Stub ,如何判断它是 Stub 还是 Mock 依赖于使用上下文,换句话说,Fake 即是 Stub 也是 Mock 。
      • Stub - Stub 是系统中现有依赖项的可控替代品。通过使用 Stub ,你可以不用处理依赖直接测试你的代码。默认情况下, 伪造对象以stub 开头。
      • Mock - Mock 对象是系统中的伪造对象,它决定单元测试是否通过或失败。Mock 会以 Fake 开头,直到被断言为止。
    • Moq4 ,使用 Moq4 模拟我们在项目中依赖对象。参考
  4. 打开视图 -> 测试资源管理器。

  5. 点击运行,得到测试结果。

  6. 至此,一个单元测试结束。

集成测试

集成测试确保应用的组件功能在包含应用的基础支持下是正确的,例如:数据库、文件系统、网络等。

  1. 新建集成测试项目。

  2. 添加工具类 Utilities 。

    using System.Collections.Generic;
    using AddressEFRepository; namespace Address.IntegrationTest
    {
    public static class Utilities
    {
    public static void InitializeDbForTests(AddressContext db)
    {
    List<Domain.Address> addresses = GetSeedingAddresses();
    db.Addresses.AddRange(addresses);
    db.SaveChanges();
    } public static void ReinitializeDbForTests(AddressContext db)
    {
    db.Addresses.RemoveRange(db.Addresses);
    InitializeDbForTests(db);
    } public static List<Domain.Address> GetSeedingAddresses()
    {
    return new List<Domain.Address>
    {
    new Domain.Address
    {
    City = "贵阳",
    County = "测试县",
    Province = "贵州省"
    },
    new Domain.Address
    {
    City = "昆明市",
    County = "武定县",
    Province = "云南省"
    },
    new Domain.Address
    {
    City = "昆明市",
    County = "五华区",
    Province = "云南省"
    }
    };
    }
    }
    }
  3. 添加 CustomWebApplicationFactory 类,

     using System;
    using System.IO;
    using System.Linq;
    using AddressEFRepository;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Mvc.Testing;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Logging; namespace Address.IntegrationTest
    {
    public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
    {
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
    string projectDir = Directory.GetCurrentDirectory();
    string configPath = Path.Combine(projectDir, "appsettings.json");
    builder.ConfigureAppConfiguration((context, conf) =>
    {
    conf.AddJsonFile(configPath);
    }); builder.ConfigureServices(services =>
    {
    ServiceDescriptor descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<AddressContext>)); if (descriptor != null)
    {
    services.Remove(descriptor);
    } services.AddDbContextPool<AddressContext>((options, context) =>
    {
    //var configuration = options.GetRequiredService<IConfiguration>();
    //string connectionString = configuration.GetConnectionString("TestAddressDb");
    //context.UseMySql(connectionString);
    context.UseInMemoryDatabase("InMemoryDbForTesting"); }); // Build the service provider.
    ServiceProvider sp = services.BuildServiceProvider();
    // Create a scope to obtain a reference to the database
    // context (ApplicationDbContext).
    using IServiceScope scope = sp.CreateScope();
    IServiceProvider scopedServices = scope.ServiceProvider;
    var db = scopedServices.GetRequiredService<AddressContext>();
    var logger = scopedServices.GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>(); // Ensure the database is created.
    db.Database.EnsureCreated(); try
    {
    // Seed the database with test data.
    Utilities.ReinitializeDbForTests(db);
    }
    catch (Exception ex)
    {
    logger.LogError(ex, "An error occurred seeding the " + "database with test messages. Error: {Message}", ex.Message);
    }
    });
    }
    }
    }
    • 这里为什么要添加 CustomWebApplicationFactory 呢?

      WebApplicationFactory 是用于在内存中引导应用程序进行端到端功能测试的工厂。通过引入自定义 CustomWebApplicationFactory 类重写 ConfigureWebHost 方法,我们可以重写我们在 StartUp 中定义的内容,换句话说我们可以在测试环境中使用正式环境的配置,同时可以重写,例如:数据库配置,数据初始化等等。
    • 如何准备测试数据?

      我们可以使用数据种子的方式加入数据,数据种子可以针对每个集成测试做数据准备。
    • 除了内存数据库,还可以使用其他数据库进行测试吗?

      可以。
  4. 添加集成测试 AddressControllerIntegrationTest 类。

     using System.Collections.Generic;
    using System.Linq;
    using System.Net.Http;
    using System.Threading.Tasks;
    using Address.Api;
    using Microsoft.AspNetCore.Mvc.Testing;
    using Newtonsoft.Json;
    using Xunit; namespace Address.IntegrationTest
    {
    public class AddressControllerIntegrationTest : IClassFixture<CustomWebApplicationFactory<Startup>>
    {
    public AddressControllerIntegrationTest(CustomWebApplicationFactory<Startup> factory)
    {
    _client = factory.CreateClient(new WebApplicationFactoryClientOptions
    {
    AllowAutoRedirect = false
    });
    } private readonly HttpClient _client; [Fact]
    public async Task Get_AllAddressAndRetrieveAddress()
    {
    const string allAddressUri = "/api/Address/GetAll";
    HttpResponseMessage allAddressesHttpResponse = await _client.GetAsync(allAddressUri); allAddressesHttpResponse.EnsureSuccessStatusCode(); string allAddressStringResponse = await allAddressesHttpResponse.Content.ReadAsStringAsync();
    var addresses = JsonConvert.DeserializeObject<IList<AddressDto.AddressDto>>(allAddressStringResponse);
    Assert.Equal(3, addresses.Count); AddressDto.AddressDto address = addresses.First();
    string retrieveUri = $"/api/Address/Retrieve?id={address.ID}";
    HttpResponseMessage addressHttpResponse = await _client.GetAsync(retrieveUri); // Must be successful.
    addressHttpResponse.EnsureSuccessStatusCode(); // Deserialize and examine results.
    string addressStringResponse = await addressHttpResponse.Content.ReadAsStringAsync();
    var addressResult = JsonConvert.DeserializeObject<AddressDto.AddressDto>(addressStringResponse);
    Assert.Equal(address.ID, addressResult.ID);
    Assert.Equal(address.Province, addressResult.Province);
    Assert.Equal(address.City, addressResult.City);
    Assert.Equal(address.County, addressResult.County);
    }
    }
    }
  5. 在测试资源管理器中运行集成测试方法。



  6. 结果。

  7. 至此,集成测试完成。需要注意的是,集成测试往往耗时比较多,所以建议能使用单元测试时就不要使用集成测试。

总结:当我们写单元测试时,一般不会同时存在 Stub 和 Mock 两种模拟对象,当同时出现这两种对象时,表明单元测试写的不合理,或者业务写的太过庞大,同时,我们可以通过单元测试驱动业务代码重构。当需要重构时,我们应尽量完成重构,不要留下欠下过多技术债务。集成测试有自身的复杂度存在,我们不要节约时间而打破单一职责原则,否则会引发不可预期后果。为了应对业务修改,我们应该在业务修改以后,进行回归测试,回归测试主要关注被修改的业务部分,同时测试用例如果有没要可以重写,运行整个和修改业务有关的测试用例集。

源码地址

.NET Core 3.0 单元测试与 Asp.Net Core 3.0 集成测试的更多相关文章

  1. 008.Adding a model to an ASP.NET Core MVC app --【在 asp.net core mvc 中添加一个model (模型)】

    Adding a model to an ASP.NET Core MVC app在 asp.net core mvc 中添加一个model (模型)2017-3-30 8 分钟阅读时长 本文内容1. ...

  2. ASP.NET Core 入门教程 9、ASP.NET Core 中间件(Middleware)入门

    一.前言 1.本教程主要内容 ASP.NET Core 中间件介绍 通过自定义 ASP.NET Core 中间件实现请求验签 2.本教程环境信息 软件/环境 说明 操作系统 Windows 10 SD ...

  3. ASP.NET Core 入门教程 4、ASP.NET Core MVC控制器入门

    一.前言 1.本教程主要内容 ASP.NET Core MVC控制器简介 ASP.NET Core MVC控制器操作简介 ASP.NET Core MVC控制器操作简介返回类型简介 ASP.NET C ...

  4. C#编译器优化那点事 c# 如果一个对象的值为null,那么它调用扩展方法时为甚么不报错 webAPI 控制器(Controller)太多怎么办? .NET MVC项目设置包含Areas中的页面为默认启动页 (五)Net Core使用静态文件 学习ASP.NET Core Razor 编程系列八——并发处理

    C#编译器优化那点事   使用C#编写程序,给最终用户的程序,是需要使用release配置的,而release配置和debug配置,有一个关键区别,就是release的编译器优化默认是启用的.优化代码 ...

  5. C#中的函数式编程:递归与纯函数(二) 学习ASP.NET Core Razor 编程系列四——Asp.Net Core Razor列表模板页面

    C#中的函数式编程:递归与纯函数(二)   在序言中,我们提到函数式编程的两大特征:无副作用.函数是第一公民.现在,我们先来深入第一个特征:无副作用. 无副作用是通过引用透明(Referential ...

  6. ASP.NET Core Razor 编辑表单 - ASP.NET Core 基础教程 - 简单教程,简单编程

    原文:ASP.NET Core Razor 编辑表单 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core Razor 编辑表单 上一章节我们介绍了标签助手和 HT ...

  7. ASP.NET Core 设置和初始化数据库 - ASP.NET Core 基础教程 - 简单教程,简单编程

    原文:ASP.NET Core 设置和初始化数据库 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core 设置和初始化数据库 上一章节中我们已经设置和配置好了 EF ...

  8. ASP.NET Core 配置 EF SQLite 支持 - ASP.NET Core 基础教程 - 简单教程,简单编程

    原文:ASP.NET Core 配置 EF SQLite 支持 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core 配置 EF SQLite 支持 上一章节我有提 ...

  9. ASP.NET Core 入门笔记10,ASP.NET Core 中间件(Middleware)入门

    一.前言 1.本教程主要内容 ASP.NET Core 中间件介绍 通过自定义 ASP.NET Core 中间件实现请求验签 2.本教程环境信息 软件/环境 说明 操作系统 Windows 10 SD ...

随机推荐

  1. 算法学习之剑指offer(四)

    题目1 题目描述 输入两棵二叉树A,B,判断B是不是A的子结构.(ps:我们约定空树不是任意一个树的子结构) /** public class TreeNode { int val = 0; Tree ...

  2. Linux之shell基础

    Shell基础 一.shell概述 1) shell是一个命令行解释器,它为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序,用户可以用shell来启动.挂起.停止甚至是编写一些程序 ...

  3. VPS虚拟专用服务器

    目录   0x00 VPS服务器概述 0x01 VPS工作原理 0x02 VPS用途 0x03 VPS优势 0x04 VPS特点 0x00 VPS服务器概述 VPS服务器(虚拟专用服务器)(" ...

  4. SQL Server Try Catch 异常捕捉

    SQL Server Try Catch 异常捕捉 背景 今天遇到一个关于try catch 使用比较有意思的问题.如下一段代码: SELECT @@TRANCOUNT AS A BEGIN TRY ...

  5. Tensorflow从开始到放弃(技术篇)

    在gpu中运行 tf.device("/gpu:1") 有时候这个是会出问题的,即便你在有名称为1的gpu时.有的操作是不能支持gpu的,应该为session添加一些配置: pyt ...

  6. 15.Nginx动静分离Rewrite

    1.什么是动静分离? 将动态请求和静态请求区分访问, 2.为什么要做动静分离? 静态由Nginx处理, 动态由PHP处理或Tomcat处理.... 因为Tomcat程序本身是用来处理jsp代码的,但t ...

  7. 三维动画形变算法(Mixed Finite Elements)

    混合有限元方法通入引入辅助变量后可以将高阶微分问题变成一系列低阶微分问题的组合.在三维网格形变问题中,我们考虑如下泛函极值问题: 其中u: Ω0 → R3是变形体的空间坐标,上述泛函极值问题对应的欧拉 ...

  8. Spark集群安装与配置

    一.Scala安装 1.https://www.scala-lang.org/download/2.11.12.html下载并复制到/home/jun下解压 [jun@master ~]$ cd sc ...

  9. Unity Dropdown

    unity DropDown控件应用很简单 代码如下 frameDpdown.options.Clear(); //Dropdown.OptionData optDataFrame = new Dro ...

  10. fenby C语言 P25

    二维数组 #include <stdio.h> int main(){ int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}}; int sum=0, ...