單元測試
本篇將結合這個系列的例子的基礎上演示在Asp.Net Core里如何使用XUnit結合Moq進行單元測試,同時對整個項目進行集成測試。
第一部分、XUnit
修改 Project.json 文件內容,增加XUnit相關的nuget包引用,並修改部分配置。
1 {
2 "version": "1.0.0-*",
3 "testRunner": "xunit", // 設置測試工具為xunit
4
5 "buildOptions": {
6 "debugType": "portable",
7 "emitEntryPoint": true
8 },
9 "dependencies": {
10 "Microsoft.NETCore.App": {
11 "type": "platform",
12 "version": "1.0.0"
13 },
14 "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
15 "Microsoft.AspNetCore.Mvc": "1.0.0",
16 "Microsoft.Extensions.Logging": "1.0.0",
17 "Microsoft.Extensions.Logging.Console": "1.0.0",
18 "Microsoft.Extensions.Logging.Debug": "1.0.0",
19 "Microsoft.Extensions.Logging.Filter": "1.0.0",
20 "NLog.Extensions.Logging": "1.0.0-rtm-alpha2",
21 "Autofac.Extensions.DependencyInjection": "4.0.0-rc3-309",
22 "Microsoft.Extensions.Configuration": "1.0.0",
23 "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0",
24 "Microsoft.Extensions.Configuration.Json": "1.0.0",
25 "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0",
26 "xunit": "2.2.0-beta2-build3300",
27 "dotnet-test-xunit": "2.2.0-preview2-build1029"
28 },
29 "frameworks": {
30 "netcoreapp1.0": {
31 // 設置兼容框架
32 "imports": [
33 "dotnet54",
34 "portable-net45+win8"
35 ]
36 }
37 }
38 }
增加一個Demo類和一個測試類
1 namespace WebApiFrame
2 {
3 public class DemoModel
4 {
5 public int Add(int a, int b)
6 {
7 return a + b;
8 }
9
10 public bool IsOdd(int num)
11 {
12 return num % 2 == 1;
13 }
14 }
15 }
1 using Xunit;
2
3 namespace WebApiFrame.Test
4 {
5 public class DemoModelTest
6 {
7 private readonly DemoModel _demo;
8
9 public DemoModelTest()
10 {
11 _demo = new DemoModel();
12 }
13
14 [Fact]
15 public void AddTest()
16 {
17 int result = _demo.Add(1, 2);
18 Assert.Equal(3, result);
19 }
20 }
21 }
打開cmd窗口,進入到項目根目錄,輸入命令 dotnet test ,將啟動單元測試,可以在輸出查看測試結果

再對另外一個方法添加單元測試代碼
1 [Theory]
2 [InlineData(1)]
3 [InlineData(2)]
4 [InlineData(3)]
5 public void IsOdd(int num)
6 {
7 bool result = _demo.IsOdd(num);
8 Assert.True(result, $"{num} is not odd.");
9 }
再次啟動單元測試,查看測試結果

結果顯示執行了四個單元測試用例,有一個失敗了。
通過比較上面兩個測試方法可以發現使用的特性標識不同,測試方法的參數列表也不相同。
[Face]特性標識表示固定輸入的測試用例,而[Theory]特性標識表示可以指定多個輸入的測試用例,結合InlineData特性標識使用。在上面的例子里,總共使用了三次InlineData特性標識,每次設定的值都不同,在執行單元測試時,設定的值會被測試框架賦值到對應的測試方法的參數里。
第二部分、Moq
在之前的例子里已經定義了如下接口和類
IUserRepository.cs
UsersController.cs
我們要對 UsersController.cs 的方法進行單元測試,同時UserRepository實例是通過構造函數依賴注入的,所以要借助Moq來模擬這個實例的生成。
在引入Moq包之前,先要修改NuGet.Config配置文件,增加package包源地址。
NuGet.Config配置文件路徑: C:\Users\{user}\AppData\Roaming\NuGet
1 <?xml version="1.0" encoding="utf-8"?> 2 <configuration> 3 <activePackageSource> 4 <add key="nuget.org" value="https://www.nuget.org/api/v2/" /> 5 </activePackageSource> 6 <packageSources> 7 <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" /> 8 9 <!-- 增加的程序包源地址 --> 10 <add key="aspnet-contrib" value="https://www.myget.org/F/aspnet-contrib/api/v3/index.json" /> 11 </packageSources> 12 </configuration>
引入Moq相關nuget包: "moq.netcore": "4.4.0-beta8"
添加單元測試類
1 using System.Collections.Generic;
2 using System.Linq;
3 using Microsoft.AspNetCore.Mvc;
4 using Moq;
5 using WebApiFrame.Controllers;
6 using WebApiFrame.Models;
7 using WebApiFrame.Repositories;
8 using Xunit;
9
10 namespace WebApiFrame.Test
11 {
12 public class UsersControllerTest
13 {
14 private readonly UsersController _controller;
15
16 public UsersControllerTest()
17 {
18 var mockRepo = new Mock<IUserRepository>();
19 mockRepo.Setup(repo => repo.GetAll()).Returns(GetUsers());
20 _controller = new UsersController(mockRepo.Object);
21 }
22
23 [Fact]
24 public void GetAllTest()
25 {
26 IActionResult actionResult = _controller.GetAll();
27 var objectResult = Assert.IsType<ObjectResult>(actionResult);
28 var result = Assert.IsAssignableFrom<IEnumerable<User>>(objectResult.Value);
29 Assert.Equal(3, result.Count());
30 }
31
32 private IEnumerable<User> GetUsers()
33 {
34 return new List<User>()
35 {
36 new User(){ Id = 1, Name = "name:1", Sex = "Male" },
37 new User(){ Id = 2, Name = "name:2", Sex = "Female" },
38 new User(){ Id = 3, Name = "name:3", Sex = "Male" },
39 };
40 }
41 }
42 }
在cmd窗口執行單元測試,查看測試結果

在一個分層結構清晰的項目里,各層之間依賴於事先約定好的接口。在多人協作開發時,大多數人都只會負責自己的那一部分模塊功能,開發進度通常情況下也不一致。當某個開發人員需要對自己的模塊進行單元測試而依賴的其他模塊還沒有開發完成時,則需要對依賴的接口通過Mock的方式提供模擬功能,從而達到在不實際依賴其他模塊的具體功能的情況下完成自己模塊的單元測試工作。
第三部分、集成測試
以上的例子只是對邏輯進行了單元測試。對於Asp.Net Core項目,還需要模擬在網站部署的情況下對各個請求入口進行測試。通常情況下可以借助Fiddler等工具完成,在.Net Core里也可以用編程的方式完成測試。
首先引入測試需要的nuget包。因為我們測試的是WebApi接口,響應內容都是json格式的字符串,所以還需要引用json序列化的nuget包。
"Microsoft.AspNetCore.TestHost": "1.0.0",
"Newtonsoft.Json": "9.0.1"
添加測試類
1 using System.Collections.Generic;
2 using System.Net.Http;
3 using System.Threading.Tasks;
4 using Microsoft.AspNetCore.Hosting;
5 using Microsoft.AspNetCore.TestHost;
6 using Newtonsoft.Json;
7 using WebApiFrame.Models;
8 using Xunit;
9
10 namespace WebApiFrame.Test
11 {
12 public class WebApiFrameTest
13 {
14 private readonly TestServer _server;
15 private readonly HttpClient _client;
16
17 public WebApiFrameTest()
18 {
19 _server = new TestServer(new WebHostBuilder().UseStartup<Startup>());
20 _client = _server.CreateClient();
21 }
22
23 [Fact]
24 public async Task GetAllTest()
25 {
26 var response = await _client.GetAsync("/api/users");
27 response.EnsureSuccessStatusCode();
28
29 var responseString = await response.Content.ReadAsStringAsync();
30 IList<User> users = JsonConvert.DeserializeObject<IList<User>>(responseString);
31
32 Assert.Equal(3, users.Count);
33 }
34
35 [Theory]
36 [InlineData(1)]
37 [InlineData(2)]
38 [InlineData(3)]
39 public async Task GetTest(int id)
40 {
41 var response = await _client.GetAsync($"/api/users/{id}");
42 response.EnsureSuccessStatusCode();
43
44 var responseString = await response.Content.ReadAsStringAsync();
45 User user = JsonConvert.DeserializeObject<User>(responseString);
46
47 Assert.NotNull(user);
48 }
49 }
50 }
在cmd窗口執行單元測試,查看測試結果

在上面的例子里,通過在一個工程里同時模擬了服務端(TestServer)和客戶端(HttpClient)的通信,從而達到了整體測試WebApi接口的目的。

