专栏首页dotNET知音.NET Core 3.0 单元测试与 Asp.Net Core 3.0 集成测试

.NET Core 3.0 单元测试与 Asp.Net Core 3.0 集成测试

单元测试与集成测试

测试必要性说明

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

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

测试框架选择

在 .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. 新建单元测试项目
  1. 新建 Class
  1. 添加测试方法 /// <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); }
    • Fake - Fake 通常被用于描述 Mock 或 Stub ,如何判断它是 Stub 还是 Mock 依赖于使用上下文,换句话说,Fake 即是 Stub 也是 Mock 。
    • Stub - Stub 是系统中现有依赖项的可控替代品。通过使用 Stub ,你可以不用处理依赖直接测试你的代码。默认情况下, 伪造对象以stub 开头。
    • Mock - Mock 对象是系统中的伪造对象,它决定单元测试是否通过或失败。Mock 会以 Fake 开头,直到被断言为止。
    • 测试方法的名字包含了测试目的、测试场景以及预期行为。
    • UseInMemoryDatabase 指明使用内存数据库。
    • 创建 createAddress 对象。
    • 创建 Stub 。在单元测试中常常会提到几个概念 Stub , Mock 和 Fake ,那么在应用中我们该如何选择呢?
    • Moq4 ,使用 Moq4 模拟我们在项目中依赖对象。参考
  2. 打开视图 -> 测试资源管理器。
  1. 点击运行,得到测试结果。
  1. 至此,一个单元测试结束。

集成测试

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

  1. 新建集成测试项目。
  1. 添加工具类 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 = "云南省" } }; } } }
  2. 添加 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 中定义的内容,换句话说我们可以在测试环境中使用正式环境的配置,同时可以重写,例如:数据库配置,数据初始化等等。
    • 如何准备测试数据? 我们可以使用数据种子的方式加入数据,数据种子可以针对每个集成测试做数据准备。
    • 除了内存数据库,还可以使用其他数据库进行测试吗? 可以。
  3. 添加集成测试 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); } } }
  4. 在测试资源管理器中运行集成测试方法。
  1. 结果。
  1. 至此,集成测试完成。需要注意的是,集成测试往往耗时比较多,所以建议能使用单元测试时就不要使用集成测试。

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

本文分享自微信公众号 - dotNET知音(AAshiyou),作者:Zhang_Xiang

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-11-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • net core WebApi——使用xUnits来实现单元测试

    从开始敲代码到现在,不停地都是在喊着记得做测试,记得自测,测试人员打回来扣你money之类的,刚开始因为心疼钱(当然还是为了代码质量),就老老实实自己写完自己跑...

    李明成
  • [吃螃蟹]基于 Blazui 的 Blazor 后台管理模板 BlazAdmin 正式尝鲜

      BlazAdmin 是一个基于Blazui的后台管理模板,无JS,无TS,非 Silverlight,非 WebForm,一个标签即可使用。   我将在下一...

    李明成
  • 分享一个基于Net Core 3.1开发的模块化的项目

    框架如何去加载所写的模块这是最核心的问题之一,好在Asp.Net Core MVC为模块化提供了一个部件管理类

    李明成
  • 制造业ERP实施应该注意

      除了资金支持,高级管理层在自上而下的项目领导方面必须发挥积极的作用,并参与与项目有关的重要决策。对项目经理(PM)而言,高级管理层还可被视为一种资源。这种资...

    明象ERP
  • Spring+SpringMVC+MyBatis+easyUI整合优化篇(三)代码测试

    前言 看到标题你可能会问为什么这一篇会谈到代码测试,不是说代码优化么?前两篇主要是讲了程序的输出及Log4j的使用,Log能够帮助我们进行bug的定位,优化开发...

    我是十三
  • 自定义Controller方法参数解析器

    这个接口中有两个方法,supportsParameter用于判断是否通过本解析器解析该参数,resolveArgument用于编写解析的逻辑,返回的对象赋值给方...

    DH镔
  • 深入浅出 Babel 上篇:架构和原理 + 实战

    这个文章系列将带大家深入浅出 Babel, 这个系列将分为上下两篇:上篇主要介绍 Babel 的架构和原理,顺便实践一下插件开发的;下篇会介绍 babel-pl...

    桃翁
  • python小应用之整理手机图片

    前几天去国图拍了一本书,一本心理学方面的书,也许你问我为什么不去买一本,或者去网上找pdf。 其实吧,关于心理学方面的书可以说在市面上一抓就是一堆,至于拍这本书...

    佛系编程人
  • python小应用之整理手机图片

    前几天去国图拍了一本书,一本心理学方面的书,也许你问我为什么不去买一本,或者去网上找pdf。其实吧,关于心理学方面的书可以说在市面上一抓就是一堆,至于拍这本书两...

    用户1564362
  • 深入浅出 Babel 上篇:架构和原理 + 实战

    这个文章系列将带大家深入浅出 Babel, 这个系列将分为上下两篇:上篇主要介绍 Babel 的架构和原理,顺便实践一下插件开发的;下篇会介绍 babel-pl...

    Nealyang

扫码关注云+社区

领取腾讯云代金券