<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在以前的.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可以支援多種平臺的測試:
為了使範例專案更加的貼近真實的專案開發,這裡採用分層的方式建立一個範例專案,建立完成後的專案結構如下圖所示:
下面講解一下每層的作用,按照從上往下的順序:
實體層裡面只有一個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; } } }
裡面封裝與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 { /// <summary> /// 資料上下文類 /// </summary> public class AppDbContext : DbContext { /// <summary> /// 通過建構函式給父類別構造傳參 /// </summary> /// <param name="options"></param> public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } public DbSet<Student> Students { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Student>().ToTable("T_Student"); modelBuilder.Entity<Student>().HasKey(p => p.ID); modelBuilder.Entity<Student>().Property(p => p.Name).HasMaxLength(32); // 新增種子資料 modelBuilder.Entity<Student>().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<AppDbContext>(options => { options.UseSqlServer(Configuration.GetSection("ConnectionString").GetSection("DbConnection").Value); });
這樣就可以使用資料遷移的方式生成資料庫了。
該專案中使用泛型倉儲,定義一個泛型倉儲介面:
using System.Collections.Generic; using System.Threading.Tasks; namespace UnitTest.IRepository { public interface IRepository<T> where T:class,new() { Task<List<T>> GetList(); Task<int?> Add(T entity); Task<int?> Update(T entity); Task<int?> Delete(T entity); } }
然後在定義IStudentRepository介面繼承自IRepository泛型介面:
using UnitTest.Model; namespace UnitTest.IRepository { public interface IStudentRepository: IRepository<Student> { } }
這裡是實現上面定義的倉儲介面:
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; /// <summary> /// 通過建構函式實現依賴注入 /// </summary> /// <param name="dbContext"></param> public StudentRepository(AppDbContext dbContext) { _dbContext = dbContext; } public async Task<int?> Add(Student entity) { _dbContext.Students.Add(entity); return await _dbContext.SaveChangesAsync(); } public async Task<int?> Delete(Student entity) { _dbContext.Students.Remove(entity); return await _dbContext.SaveChangesAsync(); } public async Task<List<Student>> GetList() { List<Student> list = new List<Student>(); list = await Task.Run<List<Student>>(() => { return _dbContext.Students.ToList(); }); return list; } public async Task<int?> 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>(student).State = Microsoft.EntityFrameworkCore.EntityState.Modified; return await _dbContext.SaveChangesAsync(); } return 0; } } }
先新增一個Value控制器,裡面只有一個Get方法,而且沒有任何的依賴關係,先進行最簡單的測試:
using Microsoft.AspNetCore.Mvc; namespace UnitTestDemo.Controllers { [Route("api/[controller]")] [ApiController] public class ValueController : ControllerBase { [HttpGet("{id}")] public ActionResult<string> Get(int id) { return $"Para is {id}"; } } }
我們在新增測試專案的時候,直接選擇使用xUnit測試專案,如下圖所示:
這樣專案建立完成以後,就會自動新增xUnit的參照:
<ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" /> </ItemGroup>
但要測試 ASP.NET Core 應用還需要新增兩個 NuGet 包:
Install-Package Microsoft.AspNetCore.App Install-Package Microsoft.AspNetCore.TestHost
上面是使用命令的方式進行安裝,也可以在管理NuGet程式包裡面進行搜尋,然後安裝。
千萬不要忘記還要引入要測試的專案。最後的專案引入是這樣的:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> <IsPackable>false</IsPackable> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.App" Version="2.2.8" /> <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="3.1.2" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" /> <PackageReference Include="coverlet.collector" Version="1.0.1" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..UnitTest.ModelUnitTest.Model.csproj" /> <ProjectReference Include="..UnitTestDemoUnitTestDemo.csproj" /> </ItemGroup> </Project>
都新增完以後,重新編譯專案,保證生成沒有錯誤。
單元測試按照從上往下的順序,一般分為三個階段:
我們在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; } /// <summary> /// 構造方法 /// </summary> public ValueTests() { var server = new TestServer(WebHost.CreateDefaultBuilder() .UseStartup<Startup>()); _client = server.CreateClient(); } [Fact] public async Task GetById_ShouldBe_Ok() { // 1、Arrange var id = 1; // 2、Act // 呼叫非同步的Get方法 var response = await _client.GetAsync($"/api/value/{id}"); // 3、Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); } } }
我們在建構函式中,通過TestServer拿到一個HttpClient物件,用它來模擬Http請求。我們寫了一個測試用例,完整演示了單元測試的Arrange、Act和Assert三個步驟。
單元測試用例寫好以後,開啟“測試資源管理器”:
在底部就可以看到測試資源管理器了:
在要測試的方法上面右鍵,選擇“執行測試”就可以進行測試了:
注意觀察測試方法前面圖示的顏色,目前是藍色的,表示測試用例還沒有執行過:
測試用例結束以後,我們在測試資源管理器裡面可以看到結果:
綠色表示測試通過。我們還可以看到執行測試用例消耗的時間。
如果測試結果和預期結果一致,那麼測試用例前面圖示的顏色也會變成綠色:
如果測試結果和預期結果不一致就會顯示紅色,然後需要修改程式碼直到出現綠色圖示。我們修改測試用例,模擬測試失敗的情況:
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; } /// <summary> /// 構造方法 /// </summary> public ValueTests() { var server = new TestServer(WebHost.CreateDefaultBuilder() .UseStartup<Startup>()); _client = server.CreateClient(); } [Fact] public async Task GetById_ShouldBe_Ok() { // 1、Arrange var 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); } } }
然後執行測試用例:
我們也可以通過新增斷點的方式在測試用例中進行偵錯。偵錯單元測試很簡單,只需要在要偵錯的方法上面右鍵選擇“偵錯測試”,如下圖所示:
其它操作就跟偵錯普通方法一樣。
除了新增斷點偵錯,我們還可以採用列印紀錄檔的方法來快速偵錯,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; } /// <summary> /// 構造方法 /// </summary> public ValueTests(ITestOutputHelper outputHelper) { var server = new TestServer(WebHost.CreateDefaultBuilder() .UseStartup<Startup>()); _client = server.CreateClient(); Output = outputHelper; } [Fact] public async Task GetById_ShouldBe_Ok() { // 1、Arrange var id = 1; // 2、Act // 呼叫非同步的Get方法 var response = await _client.GetAsync($"/api/value/{id}"); // 3、Assert // 模擬測試失敗 //Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); // 輸出返回資訊 // Output var responseText = await response.Content.ReadAsStringAsync(); Output.WriteLine(responseText); // 3、Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); } } }
這裡我們在建構函式中新增了 ITestOutputHelper 引數,xUnit 會將一個實現此介面的範例注入進來。拿到這個範例後,我們就可以用它來輸出紀錄檔了。執行(注意不是 Debug)此方法,執行結束後在測試資源管理器裡面檢視:
點選就可以看到輸出的紀錄檔了:
在上面的例子中,我們是使用的簡單的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; /// <summary> /// 通過建構函式注入 /// </summary> /// <param name="repository"></param> public StudentController(IStudentRepository repository) { _repository = repository; } /// <summary> /// get方法 /// </summary> /// <returns></returns> [HttpGet] public async Task<ActionResult<List<Student>>> Get() { return await _repository.GetList(); } } }
然後在Startup類的ConfigureServices方法中注入:
public void ConfigureServices(IServiceCollection services) { // 新增資料庫連線字串 services.AddDbContext<AppDbContext>(options => { options.UseSqlServer(Configuration.GetSection("ConnectionString").GetSection("DbConnection").Value); }); // 新增依賴注入到容器中 services.AddScoped<IStudentRepository, StudentRepository>(); 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<Startup>()); Client = server.CreateClient(); Output = outputHelper; } [Fact] public async Task Get_ShouldBe_Ok() { // 2、Act var response = await Client.GetAsync($"api/student"); // Output string context = await response.Content.ReadAsStringAsync(); Output.WriteLine(context); List<Student> list = JsonConvert.DeserializeObject<List<Student>>(context); // Assert Assert.Equal(3, list.Count); } } }
然後執行單元測試:
可以看到,控制器裡面如果有依賴關係,也是可以使用這種方式進行測試的。
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; /// <summary> /// 通過建構函式注入 /// </summary> /// <param name="repository"></param> public StudentController(IStudentRepository repository) { _repository = repository; } /// <summary> /// get方法 /// </summary> /// <returns></returns> [HttpGet] public async Task<ActionResult<List<Student>>> Get() { return await _repository.GetList(); } /// <summary> /// Post方法 /// </summary> /// <param name="entity"></param> /// <returns></returns> [HttpPost] public async Task<bool> 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<Startup>()); Client = server.CreateClient(); Output = outputHelper; } [Fact] public async Task Get_ShouldBe_Ok() { // 2、Act var response = await Client.GetAsync($"api/student"); // Output string context = await response.Content.ReadAsStringAsync(); Output.WriteLine(context); List<Student> list = JsonConvert.DeserializeObject<List<Student>>(context); // Assert Assert.Equal(3, list.Count); } [Fact] public async Task Post_ShouldBe_Ok() { // 1、Arrange Student entity = new Student() { Name="測試9", Age=25, Gender="男" }; var str = JsonConvert.SerializeObject(entity); HttpContent content = new StringContent(str); // 2、Act content.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、Assert Assert.Equal("true", responseBody); } } }
執行測試用例:
這樣一個簡單的單元測試就完成了。
我們觀察上面的兩個測試類,發現這兩個類都有一個共同的特點:都是在建構函式裡面建立一個HttpClient物件,我們可以把建立HttpClient物件抽離到一個共同的基礎類別裡面,所有的類都繼承自基礎類別。該基礎類別程式碼如下:
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using System.IO; using System.Net.Http; using UnitTestDemo; namespace TestDemo { /// <summary> /// 基礎類別 /// </summary> public class ApiControllerTestBase { /// <summary> /// 返回HttpClient物件 /// </summary> /// <returns></returns> protected HttpClient GetClient() { var builder = new WebHostBuilder() // 指定使用當前目錄 .UseContentRoot(Directory.GetCurrentDirectory()) // 使用Startup類作為啟動類 .UseStartup<Startup>() // 設定使用測試環境 .UseEnvironment("Testing"); var server = new TestServer(builder); // 建立HttpClient HttpClient 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<Startup>()); // Client = server.CreateClient(); // 從父類別裡面獲取HttpClient物件 Client = base.GetClient(); Output = outputHelper; } [Fact] public async Task Get_ShouldBe_Ok() { // 2、Act var response = await Client.GetAsync($"api/student"); // Output string context = await response.Content.ReadAsStringAsync(); Output.WriteLine(context); List<Student> list = JsonConvert.DeserializeObject<List<Student>>(context); // Assert Assert.Equal(3, list.Count); } [Fact] public async Task Post_ShouldBe_Ok() { // 1、Arrange Student entity = new Student() { Name="測試9", Age=25, Gender="男" }; var str = JsonConvert.SerializeObject(entity); HttpContent content = new StringContent(str); // 2、Act content.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、Assert Assert.Equal("true", responseBody); } } }
文章中的範例程式碼地址:https://github.com/jxl1024/UnitTest
到此這篇關於ASP.NET Core專案使用xUnit進行單元測試的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45