上一篇我們介紹了資料塑形,HATEOAS和內容協商,並在制器方法中完成了對應功能的新增;本章我們將介紹日誌和測試相關的概念,並新增對應的功能
一、全域性日誌
在第一章介紹專案結構時,有提到.NET Core啟動時預設載入了日誌服務,且在appsetting.json檔案配置了一些日誌的設定,根據設定的日誌等級的不同可以進行不同級別的資訊的顯示,但它無法做到輸出固定格式的log資訊至本地磁碟或是資料庫,所以需要我們自己手動實現,而我們可以藉助日誌框架實現。
ps:在第7章節中我們記錄的是資料處理層方法呼叫的日誌資訊,這裡記錄的則是ASP.NET Core WebAPI層級的日誌資訊,兩者有所差異
1、引入日誌框架
.NET程式中常用的日誌框架有log4net,serilog 和Nlog,這裡我們使用Serilog來實現相關功能,在BlogSystem.Core層使用NuGet安裝Serilog.AspNetCore,同時還需要搜尋Serilog.Skins安裝希望支援的功能,這裡我們希望新增對檔案和控制檯的輸出,所以選擇安裝的是Serilog.Skins.File和Serilog.Skins.Console
需要注意的是Serilog是不受appsetting.json的日誌設定影響的,且它可以根據名稱空間重寫記錄級別。還有一點需要注意的是需要手動對Serilog物件進行資源的釋放,否則在系統執行期間,無法開啟日誌檔案。
2、系統新增
在BlogSystem.Core專案中新增一個Logs資料夾,並在Program類中進行Serilog物件的新增和使用,如下:
3、全域性新增
1、這個時候其實系統已經使用Serilog替換了系統自帶的log物件,如下圖,Serilog會根據相關資訊進行高亮顯示:
2、這個時候問題就來了,我們怎麼才能進行全域性的新增呢,總不能一個方法一個方法的新增吧?還記得之前我們介紹AOP時提到的過濾器Filter嗎?ASP.NET Core中一共有五類過濾器,分別是:
- 授權過濾器Authorization Filter:優先順序最高,用於確定使用者是否獲得授權。如果請求未被授權,則授權過濾器會使管道短路;
- 資源過濾器Resource Filter:授權後執行,會在Authorization之後,Model Binding之前執行,可以實現類似快取的功能;
- 方法過濾器Action Filter:在控制器的Action方法執行之前和之後被呼叫,可以更改傳遞給操作的引數或更改從操作返回的結果;
- 異常過濾器Exception Filter:當Action方法執行過程中出現了未處理的異常,將會進入這個過濾器進行統一處理;
- 結果過濾器Result Filter:執行操作結果之前和之後執行,僅在action方法成功執行後才執行;
過濾器的具體執行順序如下:
3、這裡我們可以藉助異常過濾器實現全域性日誌功能的新增;在在BlogSystem.Core專案新增一個Filters資料夾,新增一個名為ExceptionFilter的類,繼承IExceptionFilter介面,這裡是參考老張的哲學的簡化版本,實現如下:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using Serilog;
using System;
namespace BlogSystem.Core.Filters
{
public class ExceptionsFilter : IExceptionFilter
{
private readonly ILogger<ExceptionsFilter> _logger;
public ExceptionsFilter(ILogger<ExceptionsFilter> logger)
{
_logger = logger;
}
public void OnException(ExceptionContext context)
{
try
{
//錯誤資訊
var msg = context.Exception.Message;
//錯誤堆疊資訊
var stackTraceMsg = context.Exception.StackTrace;
//返回資訊
context.Result = new InternalServerErrorObjectResult(new { msg, stackTraceMsg });
//記錄錯誤日誌
_logger.LogError(WriteLog(context.Exception));
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
finally
{
//記得釋放,否則執行時無法開啟日誌檔案
Log.CloseAndFlush();
}
}
//返回500錯誤
public class InternalServerErrorObjectResult : ObjectResult
{
public InternalServerErrorObjectResult(object value) : base(value)
{
StatusCode = StatusCodes.Status500InternalServerError;
}
}
//自定義格式內容
public string WriteLog(Exception ex)
{
return $"【異常資訊】:{ex.Message} \r\n 【異常型別】:{ex.GetType().Name} \r\n【堆疊呼叫】:{ex.StackTrace}";
}
}
}
4、在Startup類的ConfigureServices方法中進行異常處理過濾器的註冊,如下:
5、我們在控制器方法中丟擲一個異常,分別檢視效果如下,如果覺得資訊太多,可調整日誌記錄級別:
二、系統測試
這裡我們從測試的類別出發,瞭解下測試相關的內容,並新增相關的測試(介紹內容大部分來自微軟官方文件,為了更易理解,從個人習慣的角度進行了修改,如有形容不當之處,可在評論區指出)
1、測試說明及分類
1、自動測試是確保軟體應用程式按照作者期望執行操作的一種絕佳方式。軟體應用有多種型別的測試,包括單元測試、整合測試、Web測試、負載測試和其他測試。單元測試用於測試個人軟體的元件或方法,並不包括如資料庫、檔案系統和網路資源類的基礎結構測試。
當然我們可以使用編寫測試的最佳方法,如測試驅動開發(TDD)所指的先編寫單元測試,再編寫該單元測試要檢查的程式碼,就好比先編寫書籍的大綱,再編寫書籍。其主要目的是為了幫助開發人員編寫更簡單,更具可讀性的高效程式碼。兩者區別如下(來自Edison Zhou)
2、以深度(測試的細緻程度)和廣度(測試的覆蓋程度)區分, 測試分類如下(此處內容來自solenovex):
Unit Test 單元測試:它可以測試一個類或者一個類的某個功能,但其覆蓋程度較低;
Integration Test 整合測試:它的細緻程度沒有單元測試高,但是有較好的覆蓋程度,它可以測試功能的組合,以及像資料庫或檔案系統這樣的外部資源;
Subcutaneous Test 皮下測試 :其作用區域為UI層的下一層,有較好的覆蓋程度,但是深度欠佳;
UI測試:直接從UI層進行測試,覆蓋程度很高,但是深度欠佳
3、在編寫單元測試時,儘量不要引入基礎結構依賴項,這些依賴項會降低測試速度,使測試更加脆弱,我們應當將其保留供整合測試使用。可以通過遵循顯示依賴項原則和使用依賴項注入避免應用程式中的這些依賴項,還可以將單元測試保留在單獨的專案中與整合測試相分離,以確保單元測試專案沒有引用或依賴於基礎結構包。
總結下常用的單元測試和整合測試,單元測試會與外部資源隔離,以保證結果的一致性;而整合測試會依賴外部資源,且覆蓋面更廣。
2、測試的目的及特徵
1、為什麼需要測試?我們從以單元測試為例從4個方面進行說明:
- 時間人力成本:進行功能測試時,通常涉及開啟應用程式,執行一系列需要遵循的步驟來驗證預期的行為,這意味著測試人員需要了解這些步驟或聯絡熟悉該步驟的人來獲取結果。對於細微的更改或者是較大的更改,都需要重複上述過程,而單元測試只需要按一下按鈕即可執行,無需測試人員瞭解整個系統,測試結果也取決於測試執行程式而非測試人員。
- 防止錯誤迴歸:程式更改後有時會出現舊功能異常的問題,所以測試時不僅要測試新功能還要確保舊功能的正常執行。而單元測試可以確保在更改一行程式碼後重新執行整套測試,確保新程式碼不會破壞現有的功能。
- 可執行性:在給定某個輸入的情況下,特定方法的作用或行為可能不會很明顯。比如,輸入或傳遞空白字串、null後,該方法會有怎樣的行為?而當我們使用一套命名正確的單元測試,並清楚的解釋給定的輸入和預期輸出,那麼它將可以驗證其有效性。
- 減少程式碼耦合:當程式碼緊密耦合時,會難以進行單元測試,所以以建立單元測試為目的時,會在一定程度上要求我們注意程式碼的解耦
2、優質的測試需要符合哪些特徵,同樣以單元測試為例:
- 快速:成熟的專案會進行數千次的單元測試,所以應當花費非常少的時間來執行單元測試,一般來說在幾毫秒
- 獨立:單元測試應當是獨立的,可以單獨執行,不依賴檔案系統或資料庫等外部因素
- 可重複:單元測試的結果應當保持一致,即執行期間不進行更改,返回的結果應該相同
- 自檢查:測試應當在沒有人工互動的情況下,自動檢測是否通過
- 及時:編寫單元測試不應該花費過多的時間,如果花費時間較長,應當考慮另外一種更易測試的設計
在具體的執行時,我們應當遵循一些最佳實踐規則,具體請參考微軟官方文件單元測試最佳做法
3、xUnit框架介紹
常用的單元測試框架有MSTest、xUnit和NUnit,這裡我們以xUnit為例進行相關的說明
3.1、測試操作
首先我們要明確如何編寫測試程式碼,一般來說,測試分為三個主要操作:
- Arrange:意為安排或準備,這裡可以根據需求進行物件的建立或相關的設定;
- Act:意為操作,這裡可以執行獲取生產程式碼返回的結果或者是設定屬性;
- Assert:意為斷言,這裡可以用來判斷某些項是否按預期進行,即測試通過還是失敗
3.2、Assert型別
Assert時通常會對不同型別的返回值進行判斷,而在xUnit中是支援多種返回值型別的,常用的型別如下:
boolean:針對方法返回值為bool的結果,可以判斷結果是true或false
string:針對方法返回值為string的結果,可以判斷結果是否相等,是否以某字串開頭或結尾,是否包含某些字元,並支援正規表示式
數值型:針對方法返回值為數值的結果,可以判斷數值是否相等,數值是否在某個區間內,數值是否為null或非null
Collection:針對方法返回值為集合的結果,可以針對集合內所有元素或至少一個元素判斷其是否包含某某字元,兩個集合是否相等
ObjectType:針對方法返回值為某種型別的情況,可以判斷是否為預期的型別,一個類是否繼承於另一個類,兩個類是否為同一例項
Raised event:針對事件是否執行的情況,可以判斷方法內部是否執行了預期的事件
3.3、常用特性
在xUnit中還有一些常用的特性,可作用於方法或類,如下:
[Fact]:用來標註該方法為測試方法
[Trait("Name","Value")]:用來對測試方法進行分組,支援標註多個不同的組名
[Fact(Skip="忽略說明...")]:用來修飾需要忽略測試的方法
3.4 、效能相關
在測試時我們應當注意效能上的問題,針對一個物件供多個方法使用的情況,我們可以使用共享上下文
- 針對一個物件供同一類中的多個方法使用時,可以將該物件提取出來,使用IClassFixture
物件將其注入到建構函式中 - 針對一個物件供多個測試類使用的情況,可以使用ICollectionFixture
物件和[CollectionDefinition("...")]定義該物件
需要注意在使用IClassFixture和ICollectionFixture物件時應當避免多個測試方法之間相互影響的情況
3.5、資料驅動測試
在進行測試方法時,通常我們會指定輸入值和輸出值,如希望多測試幾種情況,我們可以定義多個測試方法,但這顯然不是一個最佳的實現;在合理的情況下,我們可以將引數和資料分離,如何實現?
- 方法一:使用[Theory]替換[Fact],將輸入輸出引數提取為方法引數,並使用多個[InlineData("輸入引數","輸出引數)]來標註方法
- 方法二:使用[Theory]替換[Fact],針對測試方法新增一個測試資料類,該類包含一個靜態屬性IEumerable<object[]>,將資料封裝為一個list後賦值給該屬性,並使用[MemberData(nameof(資料類的屬性),MemberType=typeof(資料類))]標註測試方法即可;
- 方法三:使用外部資料如資料庫資料/Excel資料/txt資料等,其實現原理與方法二相同,只是多了一個資料獲取封裝為list的步驟;
- 方法四:自定義一個Attribute,繼承自DataAttribute,實現其對應的方法,使用yield返回object型別的陣列;使用時只需要在測試方法上方新增[Theory]和[自定義Attribute]即可
4、測試專案新增
4.1、新增測試專案
首先我們右鍵專案解決方案選擇新增一個專案,輸入選擇xUnit後進行新增,專案命名為BlogSystem.Core.Test,如下:
專案新增完成後我們需要新增對測試專案的引用,在解決方案中右擊依賴項選擇新增BlogSystem.Core;這裡我們預期對Controller進行測試,但後續有可能會新增其他專案的測試,所以我們建立一個Controller_Test資料夾保證專案結構相對清晰。
4.2、新增測試方法
在BlogSystem.Core.Test專案的Controller_Test資料夾下新建一個命名為UserController_Should的方法;在微軟的《單元測試的最佳做法》文件中有提到,測試命名應該包括三個部分:①被測試方法的名稱②測試的方案③方案預期行為;實際使用時也可以對照測試的方法進行命名,這裡我們先不考慮最佳命名原則,僅對照測試方法進行命名,如下:
using Xunit;
namespace BlogSystem.Core.Test.Controller_Test
{
public class UserController_Should
{
[Fact]
public void Register_Test()
{
}
}
}
4.3、方案選擇
1、在進行測試時,我們可以根據實際情況使用以下方案來進行測試:
- 方案一:直接new一個Controller物件,呼叫其Action方法直接進行測試;適用於Controller沒有其他依賴項的情況;
- 方案二:當有多個依賴項時,可以藉助工具來模擬例項化時的依賴項,如Moq就是一個很好的工具;當然這需要一定的學習成本;
- 方案三:模擬Http請求的方式來呼叫API進行測試;NuGet中的Microsoft.AspNetCore.TestHost就支援這類情況;
- 方案四:自定義方法例項化所有依賴項;將測試過程種需要用到的物件放到容器中並載入,其實現較為複雜;
這裡我們以測試UserController為例,其建構函式包含了介面服務例項和HttpContext物件例項,Action方法內部又有資料庫連線操作,從嚴格意義上來講測試這類方法已經脫離了單元測試的範疇,屬於整合測試,但這類測試一定程度上可以節省我們大量的重複勞動。這裡我們選擇方案三進行相關的測試。
2、如何使用TestHost物件?先來看看它的工作流程,首先它會建立一個IHostBuilder物件,並用它建立一個TestServer物件,TestServer物件可以建立HttpClient物件,該物件支援傳送及響應請求,如下圖所示(來自solenovex):
在嘗試使用該物件的過程中我們會發現一個問題,建立IHostBuilder物件時需要指明類似Startup的配置項,因為這裡是測試環境,所以實際上會與BlogSystem.Core中的配置類StartUp存在一定的差異,因而這裡我們需要為測試新建立一個Startup配置類。
4.4、方法實現
1、我們在測試專案中新增名為TestServerFixture 的類和名為TestStartup的類,TestServerFixture 用來建立HttpClient物件並做一些準備工作,TestStartup類為配置類。然後使用Nuget安裝Microsoft.AspNetCore.TestHost;TestServerFixture 和TestStartup實現如下:
using Autofac.Extensions.DependencyInjection;
using BlogSystem.Core.Helpers;
using BlogSystem.Model;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Hosting;
using System;
using System.Net.Http;
namespace BlogSystem.Core.Test
{
public static class TestServerFixture
{
public static IHostBuilder GetTestHost()
{
return Host.CreateDefaultBuilder()
.UseServiceProviderFactory(new AutofacServiceProviderFactory())//使用autofac作為DI容器
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseTestServer()//建立TestServer——測試的關鍵
.UseEnvironment("Development")
.UseStartup<TestStartup>();
});
}
//生成帶token的httpclient
public static HttpClient GetTestClientWithToken(this IHost host)
{
var client = host.GetTestClient();
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {GenerateJwtToken()}");//把token加到Header中
return client;
}
//生成JwtToken
public static string GenerateJwtToken()
{
TokenModelJwt tokenModel = new TokenModelJwt { UserId = userData.Id, Level = userData.Level.ToString() };
var token = JwtHelper.JwtEncrypt(tokenModel);
return token;
}
//測試使用者的資料
private static readonly User userData = new User
{
Account = "jordan",
Id = new Guid("9CF2DAB5-B9DC-4910-98D8-CBB9D54E3D7B"),
Level = Level.普通使用者
};
}
}
using Autofac;
using Autofac.Extras.DynamicProxy;
using BlogSystem.Common.Helpers;
using BlogSystem.Common.Helpers.SortHelper;
using BlogSystem.Core.AOP;
using BlogSystem.Core.Filters;
using BlogSystem.Core.Helpers;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
namespace BlogSystem.Core.Test
{
public class TestStartup
{
private readonly IConfiguration _configuration;
public TestStartup(IConfiguration configuration)
{
_configuration = GetConfig(null);
//傳遞Configuration物件
JwtHelper.GetConfiguration(_configuration);
}
public void ConfigureServices(IServiceCollection services)
{
//控制器服務註冊
services.AddControllers(setup =>
{
setup.ReturnHttpNotAcceptable = true;//開啟不存在請求格式則返回406狀態碼的選項
var jsonOutputFormatter = setup.OutputFormatters.OfType<SystemTextJsonOutputFormatter>()?.FirstOrDefault();//不為空則繼續執行
jsonOutputFormatter?.SupportedMediaTypes.Add("application/vnd.company.hateoas+json");
setup.Filters.Add(typeof(ExceptionsFilter));//新增異常過濾器
}).AddXmlDataContractSerializerFormatters()//開啟輸出輸入支援XML格式
//jwt授權服務註冊
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x =>
{
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true, //驗證金鑰
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_configuration["JwtTokenManagement:secret"])),
ValidateIssuer = true, //驗證發行人
ValidIssuer = _configuration["JwtTokenManagement:issuer"],
ValidateAudience = true, //驗證訂閱人
ValidAudience = _configuration["JwtTokenManagement:audience"],
RequireExpirationTime = true, //驗證過期時間
ValidateLifetime = true, //驗證生命週期
ClockSkew = TimeSpan.Zero, //緩衝過期時間,即使配置了過期時間,也要考慮過期時間+緩衝時間
};
});
//註冊HttpContext存取器服務
services.AddHttpContextAccessor();
//自定義判斷屬性隱射關係
services.AddTransient<IPropertyMappingService, PropertyMappingService>();
services.AddTransient<IPropertyCheckService, PropertyCheckService>();
}
//configureContainer訪問AutoFac容器生成器
public void ConfigureContainer(ContainerBuilder builder)
{
//獲取程式集並註冊,採用每次請求都建立一個新的物件的模式
var assemblyBll = Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, "BlogSystem.BLL.dll"));
var assemblyDal = Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, "BlogSystem.DAL.dll"));
builder.RegisterAssemblyTypes(assemblyDal).AsImplementedInterfaces().InstancePerDependency();
//註冊攔截器
builder.RegisterType<LogAop>();
//對目標型別啟用動態代理,並注入自定義攔截器攔截BLL
builder.RegisterAssemblyTypes(assemblyBll).AsImplementedInterfaces().InstancePerDependency()
.EnableInterfaceInterceptors().InterceptedBy(typeof(LogAop));
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler(builder =>
{
builder.Run(async context =>
{
context.Response.StatusCode = 500;
await context.Response.WriteAsync("Unexpected Error!");
});
});
}
app.UseRouting();
//新增認證中介軟體
app.UseAuthentication();
//新增授權中介軟體
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
private IConfiguration GetConfig(string environmentName)
{
var path = Microsoft.DotNet.PlatformAbstractions.ApplicationEnvironment.ApplicationBasePath;
IConfigurationBuilder builder = new ConfigurationBuilder().SetBasePath(path)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
if (!string.IsNullOrWhiteSpace(environmentName))
{
builder = builder.AddJsonFile($"appsettings.{environmentName}.json", optional: true);
}
builder = builder.AddEnvironmentVariables();
return builder.Build();
}
}
}
2、這裡對UserController中的註冊、登入、獲取使用者資訊方法進行測試,實際上這裡的斷言並不嚴謹,會產生什麼後果?請繼續往下看
using BlogSystem.Model.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace BlogSystem.Core.Test.Controller_Test
{
public class UserController_Should
{
const string _mediaType = "application/json";
readonly Encoding _encoding = Encoding.UTF8;
/// <summary>
/// 使用者註冊
/// </summary>
[Fact]
public async Task Register_Test()
{
// 1、Arrange
var data = new RegisterViewModel { Account = "test", Password = "123456", RequirePassword = "123456" };
StringContent content = new StringContent(JsonConvert.SerializeObject(data), _encoding, _mediaType);
using var host = await TestServerFixture.GetTestHost().StartAsync();//啟動TestServer
// 2、Act
var response = await host.GetTestClient().PostAsync($"http://localhost:5000/api/user/register", content);
var result = await response.Content.ReadAsStringAsync();
// 3、Assert
Assert.DoesNotContain("使用者已存在", result);
}
/// <summary>
/// 使用者登入
/// </summary>
[Fact]
public async Task Login_Test()
{
var data = new LoginViewModel { Account = "jordan", Password = "123456" };
StringContent content = new StringContent(JsonConvert.SerializeObject(data), _encoding, _mediaType);
var host = await TestServerFixture.GetTestHost().StartAsync();//啟動TestServer
var response = await host.GetTestClientWithToken().PostAsync($"http://localhost:5000/api/user/Login", content);
var result = await response.Content.ReadAsStringAsync();
Assert.DoesNotContain("賬號或密碼錯誤!", result);
}
/// <summary>
/// 獲取使用者資訊
/// </summary>
[Fact]
public async Task UserInfo_Test()
{
string id = "jordan";
using var host = await TestServerFixture.GetTestHost().StartAsync();//啟動TestServer
var client = host.GetTestClient();
var response = await client.GetAsync($"http://localhost:5000/api/user/{id}");
var result = response.StatusCode;
Assert.True(Equals(HttpStatusCode.OK, result)|| Equals(HttpStatusCode.NotFound, result));
}
}
}
4.5、異常及解決
1、新增完上述的測試方法後,我們使用開啟Visual Studio自帶的測試資源管理器,點選執行所有測試,發現提示錯誤無法載入BLL?在原先的BlogSystem.Core的StartUp類中我們是載入BLL和DAL專案的dll來達到解耦的目的,所以做了一個將dll輸出到Core專案bin資料夾的動作,但是在測試專案的TestStarup類中,我們是無法載入到BLL和DAL的。我嘗試將BLL和DAL同時輸出到兩個路徑下,但未找到對應的方法,所以這裡我採用了最簡單的解決方法,測試專案新增了對DAL和BLL的引用。再次執行,如下圖,似乎成功了??
2、我們在測試方法內部打上斷點,右擊測試方法,選擇除錯測試,結果發現response引數為空,只應Assert不嚴謹導致看上去沒有問題;在各種查詢後,我終於找到了解決辦法,在TestStarup類的ConfigureServices方法內部service.AddControllers方法最後加上這麼一句話即可解決 .AddApplicationPart(Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, "BlogSystem.Core.dll")))
3、再次執行測試方法,成功!但是又發現了另外一個問題,這裡我們只是測試,但是資料庫中卻出現了我們測試新增的test賬號,如何解決?我們可以使用Microsoft.EntityFrameworkCore.InMemory庫 ,它支援使用記憶體資料庫進行測試,這裡暫未新增,有興趣的朋友可以自行研究。
本章完~
本人知識點有限,若文中有錯誤的地方請及時指正,方便大家更好的學習和交流。
本文部分內容參考了網路上的視訊內容和文章,僅為學習和交流,視訊地址如下:
老張的哲學,系列教程一目錄:.netcore+vue 前後端分離
我想吃晚飯,ASP.NET Core搭建多層網站架構【12-xUnit單元測試之整合測試】
solenovex,使用 xUnit.NET 對 .NET Core 專案進行單元測試
solenovex,ASP.NET Core Web API 整合測試
微軟官方文件,.NET Core 和 .NET Standard 中的單元測試
Edison Zhou,.NET單元測試的藝術