相信你们在看到单元测试与集成测试这个标题时,会有不少感慨,咱们无数次的在实践中提到要作单元测试、集成测试,可是大多数项目都没有作或者仅建了项目文件。这里有客观缘由,已经接近交付日期了,咱们没时间作白盒测试了。也有主观缘由,面对业务复杂的代码咱们不知道如何入手作单元测试,不如就留给黑盒测试吧。可是,当咱们的代码没法进行单元测试的时候,每每就是代码开始散发出坏味道的时候。久而久之,将欠下技术债务。在实践过程当中,技术债务经常会存在,关键在于什么时候偿还,如何偿还。html
上图说明了随着时间的推移开发/维护难度的变化。git
在 .NET Core 中,提供了 xUnit 、NUnit 、 MSTest 三种单元测试框架。github
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 在使用上相对其它两个框架来讲提供更多的便利性。可是这里最终实现仍是看我的习惯以选择。数据库
新建单元测试项目
json
新建 Class
api
添加测试方法网络
/// <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); }
打开视图 -> 测试资源管理器。
app
点击运行,获得测试结果。
框架
至此,一个单元测试结束。async
集成测试确保应用的组件功能在包含应用的基础支持下是正确的,例如:数据库、文件系统、网络等。
新建集成测试项目。
添加工具类 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 = "云南省" } }; } } }
添加 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); } }); } } }
添加集成测试 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); } } }
在测试资源管理器中运行集成测试方法。
结果。
至此,集成测试完成。须要注意的是,集成测试每每耗时比较多,因此建议能使用单元测试时就不要使用集成测试。
总结:当咱们写单元测试时,通常不会同时存在 Stub 和 Mock 两种模拟对象,当同时出现这两种对象时,代表单元测试写的不合理,或者业务写的太过庞大,同时,咱们能够经过单元测试驱动业务代码重构。当须要重构时,咱们应尽可能完成重构,不要留下欠下过多技术债务。集成测试有自身的复杂度存在,咱们不要节约时间而打破单一职责原则,不然会引起不可预期后果。为了应对业务修改,咱们应该在业务修改之后,进行回归测试,回归测试主要关注被修改的业务部分,同时测试用例若是有没要能够重写,运行整个和修改业务有关的测试用例集。