单元测试与集成测试

测试必要性说明

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

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

测试框架选择

在 .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. JVM系列一:JVM内存模型

    今天起开始总结JVM.自己也看了好多JVM相关的知识,在此做个总结. 打算分为五个部分来讲:JVM内存模型.JVM类加载机制.JVM垃圾回收机制.JVM启动参数设置及优化.JVM其他相关. 今天首先来 ...

  2. 新手入门HTML5开发,你必须先搞懂这6个问题

    凭借着跨平台,实时更新,无需安装,易于分发等众多优点,HTML5受到越来越多企业的青睐.而凭借着入门相对简单的优势,很多人编程初学者都选择学习HTML5.但对于初学者来说,学习HTML5之前,会有很多 ...

  3. Unix 线程共享创建进程打开的文件资源(1)

    执行环境:Linux ubuntu 4.4.0-31-generic #50-Ubuntu SMP Wed Jul 13 00:07:12 UTC 2016 x86_64 x86_64 x86_64 ...

  4. DP题 总结 [更新中]

    建设中 ... 预防针 : 本蒟蒻代码风格清奇(⊙﹏⊙)b 一.选学霸 题目描述 老师想从N名学生中选M人当学霸,但有K对人实力相当,如果实力相当的人中,一部分被选上,另一部分没有,同学们就会抗议.所 ...

  5. TestLink+Jenkins在Ubuntu16.04搭建集成测试环境

    序章 序1:TestLink和TestLink-API-Python-client 目前TestLink的最新版本是1.9.19 TestLink-API-Python-client支持的TestLi ...

  6. opencv::AKAZE检测与匹配

    AKAZE局部匹配 AKAZE局部匹配介绍 AOS 构造尺度空间 Hessian矩阵特征点检测 方向指定基于一阶微分图像 描述子生成 与SIFT.SUFR比较 更加稳定 非线性尺度空间 AKAZE速度 ...

  7. 15.Nginx动静分离Rewrite

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

  8. 每日温度(LeetCode Medium难度算法题)题解

    LeetCode 题号739中等难度 每日温度 题目描述: 根据每日 气温 列表,请重新生成一个列表,对应位置的输入是你需要再等待多久温度才会升高超过该日的天数.如果之后都不会升高,请在该位置用 0 ...

  9. [洛谷P3613]睡觉困难综合症

    写码30min,调码3h的题.. 好在最后查出来了 , , n, x, y, z); 改成了 , , n, mark[x], y, z); 然后$40\rightarrow 100$ #include ...

  10. dubbo初学采坑记

    写在前面的话 dubbo 现在是apache组织旗下的项目,相信国内也有很多人使用.最近一个同事离职,我就接手了他的项目.远程通讯就是用的dubbo框架来实现的.使用Intelij idea 写了一个 ...