ASP.NET|ASP.NET Core项目使用xUnit进行单元测试

目录

  • 一、前言
  • 二、创建示例项目
    • 1、UnitTest.Model
    • 2、UnitTest.Data
    • 3、UnitTest.IRepository
    • 4、UnitTest.Repository
    • 5、UnitTestDemo
    • 6、TestDemo
  • 三、编写单元测试
    • 1、运行单元测试
    • 2、调试单元测试

一、前言 在以前的.NET Framework项目中,我们也写过一些单元测试的项目,而在ASP.NET Core 这种Web或者API应用程序中要做单元测试是很方便的。
这篇文章主要讲解如何使用xUnit对ASP.NET Core应用程序做单元测试。.NET Core中常用的测试工具还有NUnit和MSTest。
xUnit是一个测试框架,可以针对.net/.net core项目进行测试。测试项目需要引用被测试的项目,从而对其进行测试。测试项目同时需要引用xUnit库。测试编写好后,用Test Runner来运行测试。Test Runner可以读取测试代码,并且会知道我们所使用的测试框架,然后执行,并显示结果。目前可用的Test Runner包括vs自带的Test Explorer,或者dotnet core命令行,以及第三方工具,例如resharper等。
xUnit可以支持多种平台的测试:
  • .NET Framework
  • .NET Core
  • .NET Standard
  • UWP
  • Xamarin

二、创建示例项目 为了使示例项目更加的贴近真实的项目开发,这里采用分层的方式创建一个示例项目,创建完成后的项目结构如下图所示:
ASP.NET|ASP.NET Core项目使用xUnit进行单元测试
文章图片

下面讲解一下每层的作用,按照从上往下的顺序:
  • TestDemo:从名字就可以看出来,这是一个单元测试的项目,针对控制器进行测试。
  • UnitTest.Data:数据访问,封装与EntityFrameworkCore相关的操作。
  • UnitTest.IRepository:泛型仓储接口,封装基础的增删改查。
  • UnitTest.Model:实体层,定义项目中使用到的所有实体。
  • UnitTest.Repository:泛型仓储接口实现层,实现接口里面定义的方法。
  • UnitTestDemo:ASP.NET Core WebApi,提供API接口。

1、UnitTest.Model
实体层里面只有一个Student类:
using System; using System.Collections.Generic; using System.Text; namespace UnitTest.Model{public class Student{public int ID { get; set; }public string Name { get; set; }public int Age { get; set; }public string Gender { get; set; }}}


2、UnitTest.Data
里面封装与EF Core有关的操作,首先需要引入Microsoft.EntityFrameworkCore、Microsoft.EntityFrameworkCore.SqlServer、Microsoft.EntityFrameworkCore.Tools三个NuGet包,直接在管理NuGet程序包里面引入,这里不在讲述。
引入相关NuGet包以后,我们创建数据上下文类,该类继承自EF Core的DbContext,里面设置表名和一些属性:
using Microsoft.EntityFrameworkCore; using UnitTest.Model; namespace UnitTest.Data{/// /// 数据上下文类/// public class AppDbContext : DbContext{/// /// 通过构造函数给父类构造传参/// /// public AppDbContext(DbContextOptions options) : base(options){}public DbSet Students { get; set; }protected override void OnModelCreating(ModelBuilder modelBuilder){modelBuilder.Entity().ToTable("T_Student"); modelBuilder.Entity().HasKey(p => p.ID); modelBuilder.Entity().Property(p => p.Name).HasMaxLength(32); // 添加种子数据modelBuilder.Entity().HasData(new Student(){ID = 1,Name = "测试1",Age = 20,Gender = "男"},new Student(){ID = 2,Name = "测试2",Age = 22,Gender = "女"},new Student(){ID = 3,Name = "测试3",Age = 23,Gender = "男"}); base.OnModelCreating(modelBuilder); }}}

这里采用数据迁移的方式生成数据库,需要在API项目中引入Microsoft.EntityFrameworkCore、Microsoft.EntityFrameworkCore.SqlServer、Microsoft.EntityFrameworkCore.Tools三个NuGet包。引入方式同上。
然后在API项目的appsettings.json文件里面添加数据库链接字符串:
{"Logging": {"LogLevel": {"Default": "Information","Microsoft": "Warning","Microsoft.Hosting.Lifetime": "Information"}},"AllowedHosts": "*",// 数据库连接字符串"ConnectionString": {"DbConnection": "Initial Catalog=TestDb; User Id=sa; Password=1234; Data Source=.; Connection Timeout=10; "}}

在JSON文件中添加完连接字符串以后,修改Startup类的ConfigureServices方法,在里面配置使用在json文件中添加的连接字符串:
// 添加数据库连接字符串services.AddDbContext(options => {options.UseSqlServer(Configuration.GetSection("ConnectionString").GetSection("DbConnection").Value); });

这样就可以使用数据迁移的方式生成数据库了。

3、UnitTest.IRepository
该项目中使用泛型仓储,定义一个泛型仓储接口:
using System.Collections.Generic; using System.Threading.Tasks; namespace UnitTest.IRepository{public interface IRepository where T:class,new(){Task> GetList(); Task Add(T entity); Task Update(T entity); Task Delete(T entity); }}

然后在定义IStudentRepository接口继承自IRepository泛型接口:
using UnitTest.Model; namespace UnitTest.IRepository{public interface IStudentRepository: IRepository{}}


4、UnitTest.Repository
这里是实现上面定义的仓储接口:
using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using UnitTest.Data; using UnitTest.IRepository; using UnitTest.Model; namespace UnitTest.Repository{public class StudentRepository : IStudentRepository{private readonly AppDbContext _dbContext; /// /// 通过构造函数实现依赖注入/// /// public StudentRepository(AppDbContext dbContext){_dbContext = dbContext; }public async Task Add(Student entity){_dbContext.Students.Add(entity); return await _dbContext.SaveChangesAsync(); }public async Task Delete(Student entity){_dbContext.Students.Remove(entity); return await _dbContext.SaveChangesAsync(); }public async Task GetList(){List list = new List(); list = await Task.Run(() => {return _dbContext.Students.ToList(); }); return list; }public async Task Update(Student entity){Student student = _dbContext.Students.Find(entity.ID); if (student != null){student.Name = entity.Name; student.Age = entity.Age; student.Gender = entity.Gender; _dbContext.Entry(student).State = Microsoft.EntityFrameworkCore.EntityState.Modified; return await _dbContext.SaveChangesAsync(); }return 0; }}}


5、UnitTestDemo
先添加一个Value控制器,里面只有一个Get方法,而且没有任何的依赖关系,先进行最简单的测试:
using Microsoft.AspNetCore.Mvc; namespace UnitTestDemo.Controllers{[Route("api/[controller]")][ApiController]public class ValueController : ControllerBase{[HttpGet("{id}")]public ActionResult Get(int id){return $"Para is {id}"; }}}


6、TestDemo
我们在添加测试项目的时候,直接选择使用xUnit测试项目,如下图所示:
ASP.NET|ASP.NET Core项目使用xUnit进行单元测试
文章图片

这样项目创建完成以后,就会自动添加xUnit的引用:

但要测试 ASP.NET Core 应用还需要添加两个 NuGet 包:
Install-Package Microsoft.AspNetCore.AppInstall-Package Microsoft.AspNetCore.TestHost

上面是使用命令的方式进行安装,也可以在管理NuGet程序包里面进行搜索,然后安装。
千万不要忘记还要引入要测试的项目。最后的项目引入是这样的:
netcoreapp3.1false

都添加完以后,重新编译项目,保证生成没有错误。

三、编写单元测试 单元测试按照从上往下的顺序,一般分为三个阶段:
  • Arrange:准备阶段。这个阶段做一些准备工作,例如创建对象实例,初始化数据等。
  • Act:行为阶段。这个阶段是用准备好的数据去调用要测试的方法。
  • Assert:断定阶段。这个阶段就是把调用目标方法的返回值和预期的值进行比较,如果和预期值一致则测试通过,否则测试失败。
我们在API项目中添加了一个Value控制器,我们以Get方法作为测试目标。一般一个单元测试方法就是一个测试用例。
我们在测试项目中添加一个ValueTest测试类,然后编写一个单元测试方法,这里是采用模拟HTTPClient发送Http请求的方式进行测试:
using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using System.Net; using System.Net.Http; using System.Threading.Tasks; using UnitTestDemo; using Xunit; namespace TestDemo{public class ValueTests{public HttpClient _client { get; }/// /// 构造方法/// public ValueTests(){var server = new TestServer(WebHost.CreateDefaultBuilder().UseStartup()); _client = server.CreateClient(); }[Fact]public async Task GetById_ShouldBe_Ok(){// 1、Arrangevar id = 1; // 2、Act// 调用异步的Get方法var response = await _client.GetAsync($"/api/value/{id}"); // 3、AssertAssert.Equal(HttpStatusCode.OK, response.StatusCode); }}}

我们在构造函数中,通过TestServer拿到一个HttpClient对象,用它来模拟Http请求。我们写了一个测试用例,完整演示了单元测试的Arrange、Act和Assert三个步骤。

1、运行单元测试
单元测试用例写好以后,打开“测试资源管理器”:
ASP.NET|ASP.NET Core项目使用xUnit进行单元测试
文章图片

在底部就可以看到测试资源管理器了:
ASP.NET|ASP.NET Core项目使用xUnit进行单元测试
文章图片

在要测试的方法上面右键,选择“运行测试”就可以进行测试了:
ASP.NET|ASP.NET Core项目使用xUnit进行单元测试
文章图片

注意观察测试方法前面图标的颜色,目前是蓝色的,表示测试用例还没有运行过:
ASP.NET|ASP.NET Core项目使用xUnit进行单元测试
文章图片

测试用例结束以后,我们在测试资源管理器里面可以看到结果:
ASP.NET|ASP.NET Core项目使用xUnit进行单元测试
文章图片

绿色表示测试通过。我们还可以看到执行测试用例消耗的时间。
如果测试结果和预期结果一致,那么测试用例前面图标的颜色也会变成绿色:
ASP.NET|ASP.NET Core项目使用xUnit进行单元测试
文章图片

如果测试结果和预期结果不一致就会显示红色,然后需要修改代码直到出现绿色图标。我们修改测试用例,模拟测试失败的情况:
using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using System.Net; using System.Net.Http; using System.Threading.Tasks; using UnitTestDemo; using Xunit; namespace TestDemo{public class ValueTests{public HttpClient _client { get; }/// /// 构造方法/// public ValueTests(){var server = new TestServer(WebHost.CreateDefaultBuilder().UseStartup()); _client = server.CreateClient(); }[Fact]public async Task GetById_ShouldBe_Ok(){// 1、Arrangevar id = 1; // 2、Act// 调用异步的Get方法var response = await _client.GetAsync($"/api/value/{id}"); //// 3、Assert//Assert.Equal(HttpStatusCode.OK, response.StatusCode); // 3、Assert// 模拟测试失败Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); }}}

然后运行测试用例:
ASP.NET|ASP.NET Core项目使用xUnit进行单元测试
文章图片


2、调试单元测试
我们也可以通过添加断点的方式在测试用例中进行调试。调试单元测试很简单,只需要在要调试的方法上面右键选择“调试测试”,如下图所示:
ASP.NET|ASP.NET Core项目使用xUnit进行单元测试
文章图片

其它操作就跟调试普通方法一样。
除了添加断点调试,我们还可以采用打印日志的方法来快速调试,xUnit可以很方便地做到这一点。我们修改ValueTest类:
using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using System.Net; using System.Net.Http; using System.Threading.Tasks; using UnitTestDemo; using Xunit; using Xunit.Abstractions; namespace TestDemo{public class ValueTests{public HttpClient _client { get; }public ITestOutputHelper Output { get; }/// /// 构造方法/// public ValueTests(ITestOutputHelper outputHelper){var server = new TestServer(WebHost.CreateDefaultBuilder().UseStartup()); _client = server.CreateClient(); Output = outputHelper; }[Fact]public async Task GetById_ShouldBe_Ok(){// 1、Arrangevar id = 1; // 2、Act// 调用异步的Get方法var response = await _client.GetAsync($"/api/value/{id}"); // 3、Assert// 模拟测试失败//Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); // 输出返回信息// Outputvar responseText = await response.Content.ReadAsStringAsync(); Output.WriteLine(responseText); // 3、AssertAssert.Equal(HttpStatusCode.OK, response.StatusCode); }}}

这里我们在构造函数中添加了 ITestOutputHelper 参数,xUnit 会将一个实现此接口的实例注入进来。拿到这个实例后,我们就可以用它来输出日志了。运行(注意不是 Debug)此方法,运行结束后在测试资源管理器里面查看:
ASP.NET|ASP.NET Core项目使用xUnit进行单元测试
文章图片

点击就可以看到输出的日志了:
ASP.NET|ASP.NET Core项目使用xUnit进行单元测试
文章图片

在上面的例子中,我们是使用的简单的Value控制器进行测试,控制器里面没有其他依赖关系,如果控制器里面有依赖关系该如何测试呢?方法还是一样的,我们新建一个Student控制器,里面依赖IStudentRepository接口,代码如下:
using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using UnitTest.IRepository; using UnitTest.Model; namespace UnitTestDemo.Controllers{[Route("api/student")][ApiController]public class StudentController : ControllerBase{private readonly IStudentRepository _repository; /// /// 通过构造函数注入/// /// public StudentController(IStudentRepository repository){_repository = repository; }/// /// get方法/// /// [HttpGet]public async Task>> Get(){return await _repository.GetList(); }}}

然后在Startup类的ConfigureServices方法中注入:
public void ConfigureServices(IServiceCollection services){// 添加数据库连接字符串services.AddDbContext(options => {options.UseSqlServer(Configuration.GetSection("ConnectionString").GetSection("DbConnection").Value); }); // 添加依赖注入到容器中services.AddScoped(); services.AddControllers(); }

在单元测试项目中添加StudentTest类:
using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Newtonsoft.Json; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using UnitTest.Model; using UnitTestDemo; using Xunit; using Xunit.Abstractions; namespace TestDemo{public class StudentTest{public HttpClient Client { get; }public ITestOutputHelper Output { get; }public StudentTest(ITestOutputHelper outputHelper){var server = new TestServer(WebHost.CreateDefaultBuilder().UseStartup()); Client = server.CreateClient(); Output = outputHelper; }[Fact]public async Task Get_ShouldBe_Ok(){// 2、Actvar response = await Client.GetAsync($"api/student"); // Outputstring context = await response.Content.ReadAsStringAsync(); Output.WriteLine(context); List list = JsonConvert.DeserializeObject(context); // AssertAssert.Equal(3, list.Count); }}}

然后运行单元测试:
ASP.NET|ASP.NET Core项目使用xUnit进行单元测试
文章图片

可以看到,控制器里面如果有依赖关系,也是可以使用这种方式进行测试的。
Post方法也可以使用同样的方式进行测试,修改控制器,添加Post方法:
using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using UnitTest.IRepository; using UnitTest.Model; namespace UnitTestDemo.Controllers{[Route("api/student")][ApiController]public class StudentController : ControllerBase{private readonly IStudentRepository _repository; /// /// 通过构造函数注入/// /// public StudentController(IStudentRepository repository){_repository = repository; }/// /// get方法/// /// [HttpGet]public async Task>> Get(){return await _repository.GetList(); }/// /// Post方法/// /// /// [HttpPost]public async Task Post([FromBody]Student entity){int? result = await _repository.Add(entity); if(result==null){return false; }else{return result > 0 ? true : false; }}}}

在增加一个Post的测试方法:
using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Newtonsoft.Json; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using UnitTest.Model; using UnitTestDemo; using Xunit; using Xunit.Abstractions; namespace TestDemo{public class StudentTest{public HttpClient Client { get; }public ITestOutputHelper Output { get; }public StudentTest(ITestOutputHelper outputHelper){var server = new TestServer(WebHost.CreateDefaultBuilder().UseStartup()); Client = server.CreateClient(); Output = outputHelper; }[Fact]public async Task Get_ShouldBe_Ok(){// 2、Actvar response = await Client.GetAsync($"api/student"); // Outputstring context = await response.Content.ReadAsStringAsync(); Output.WriteLine(context); List list = JsonConvert.DeserializeObject(context); // AssertAssert.Equal(3, list.Count); }[Fact]public async Task Post_ShouldBe_Ok(){// 1、ArrangeStudent entity = new Student(){Name="测试9",Age=25,Gender="男"}; var str = JsonConvert.SerializeObject(entity); HttpContent content = new StringContent(str); // 2、Actcontent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); HttpResponseMessage response = await Client.PostAsync("api/student", content); string responseBody = await response.Content.ReadAsStringAsync(); Output.WriteLine(responseBody); // 3、AssertAssert.Equal("true", responseBody); }}}

运行测试用例:
ASP.NET|ASP.NET Core项目使用xUnit进行单元测试
文章图片

这样一个简单的单元测试就完成了。
我们观察上面的两个测试类,发现这两个类都有一个共同的特点:都是在构造函数里面创建一个HttpClient对象,我们可以把创建HttpClient对象抽离到一个共同的基类里面,所有的类都继承自基类。该基类代码如下:
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using System.IO; using System.Net.Http; using UnitTestDemo; namespace TestDemo{/// /// 基类/// public class ApiControllerTestBase{/// /// 返回HttpClient对象/// /// protected HttpClient GetClient(){var builder = new WebHostBuilder()// 指定使用当前目录.UseContentRoot(Directory.GetCurrentDirectory())// 使用Startup类作为启动类.UseStartup()// 设置使用测试环境.UseEnvironment("Testing"); var server = new TestServer(builder); // 创建HttpClientHttpClient client = server.CreateClient(); return client; }}}

然后修改StudentTest类,使该类继承自上面创建的基类:
using Newtonsoft.Json; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using UnitTest.Model; using Xunit; using Xunit.Abstractions; namespace TestDemo{public class StudentTest: ApiControllerTestBase{public HttpClient Client { get; }public ITestOutputHelper Output { get; }public StudentTest(ITestOutputHelper outputHelper){// var server = new TestServer(WebHost.CreateDefaultBuilder()//.UseStartup()); // Client = server.CreateClient(); // 从父类里面获取HttpClient对象Client = base.GetClient(); Output = outputHelper; }[Fact]public async Task Get_ShouldBe_Ok(){// 2、Actvar response = await Client.GetAsync($"api/student"); // Outputstring context = await response.Content.ReadAsStringAsync(); Output.WriteLine(context); List list = JsonConvert.DeserializeObject(context); // AssertAssert.Equal(3, list.Count); }[Fact]public async Task Post_ShouldBe_Ok(){// 1、ArrangeStudent entity = new Student(){Name="测试9",Age=25,Gender="男"}; var str = JsonConvert.SerializeObject(entity); HttpContent content = new StringContent(str); // 2、Actcontent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); HttpResponseMessage response = await Client.PostAsync("api/student", content); string responseBody = await response.Content.ReadAsStringAsync(); Output.WriteLine(responseBody); // 3、AssertAssert.Equal("true", responseBody); }}}

文章中的示例代码地址:https://github.com/jxl1024/UnitTest
【ASP.NET|ASP.NET Core项目使用xUnit进行单元测试】到此这篇关于ASP.NET Core项目使用xUnit进行单元测试的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

    推荐阅读