學習ASP.NET Core(10)-全局日誌與xUnit系統測試

上一篇咱們介紹了數據塑形,HATEOAS和內容協商,並在制器方法中完成了對應功能的添加;本章咱們將介紹日誌和測試相關的概念,並添加對應的功能html

1、全局日誌

在第一章介紹項目結構時,有提到.NET Core啓動時默認加載了日誌服務,且在appsetting.json文件配置了一些日誌的設置,根據設置的日誌等級的不一樣能夠進行不一樣級別的信息的顯示,但它沒法作到輸出固定格式的log信息至本地磁盤或是數據庫,因此須要咱們本身手動實現,而咱們能夠藉助日誌框架實現。vue

ps:在第7章節中咱們記錄的是數據處理層方法調用的日誌信息,這裏記錄的則是ASP.NET Core WebAPI層級的日誌信息,二者有所差別web

一、引入日誌框架

.NET程序中經常使用的日誌框架有log4net,serilog 和Nlog,這裏咱們使用Serilog來實現相關功能,在BlogSystem.Core層使用NuGet安裝Serilog.AspNetCore,同時還須要搜索Serilog.Skins安裝但願支持的功能,這裏咱們但願添加對文件和控制檯的輸出,因此選擇安裝的是Serilog.Skins.File和Serilog.Skins.Console正則表達式

須要注意的是Serilog是不受appsetting.json的日誌設置影響的,且它能夠根據命名空間重寫記錄級別。還有一點須要注意的是須要手動對Serilog對象進行資源的釋放,不然在系統運行期間,沒法打開日誌文件。數據庫

二、系統添加

在BlogSystem.Core項目中添加一個Logs文件夾,並在Program類中進行Serilog對象的添加和使用,以下:json

三、全局添加

一、這個時候其實系統已經使用Serilog替換了系統自帶的log對象,以下圖,Serilog會根據相關信息進行高亮顯示:c#

二、這個時候問題就來了,咱們怎麼才能進行全局的添加呢,總不能一個方法一個方法的添加吧?還記得以前咱們介紹AOP時提到的過濾器Filter嗎?ASP.NET Core中一共有五類過濾器,分別是:後端

  • 受權過濾器Authorization Filter:優先級最高,用於肯定用戶是否得到受權。若是請求未被受權,則受權過濾器會使管道短路;
  • 資源過濾器Resource Filter:受權後運行,會在Authorization以後,Model Binding以前執行,能夠實現相似緩存的功能;
  • 方法過濾器Action Filter:在控制器的Action方法執行以前和以後被調用,能夠更改傳遞給操做的參數或更改從操做返回的結果;
  • 異常過濾器Exception Filter:當Action方法執行過程當中出現了未處理的異常,將會進入這個過濾器進行統一處理;
  • 結果過濾器Result Filter:執行操做結果以前和以後運行,僅在action方法成功執行後才運行;

過濾器的具體執行順序以下:api

三、這裏咱們能夠藉助異常過濾器實現全局日誌功能的添加;在在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}";
        }
    }
}

四、在Startup類的ConfigureServices方法中進行異常處理過濾器的註冊,以下:

五、咱們在控制器方法中拋出一個異常,分別查看效果以下,若是以爲信息太多,可調整日誌記錄級別:

2、系統測試

這裏咱們從測試的類別出發,瞭解下測試相關的內容,並添加相關的測試(介紹內容大部分來自微軟官方文檔,爲了更易理解,從我的習慣的角度進行了修改,若有形容不當之處,可在評論區指出)

一、測試說明及分類

一、自動測試是確保軟件應用程序按照做者指望執行操做的一種絕佳方式。軟件應用有多種類型的測試,包括單元測試、集成測試、Web測試、負載測試和其餘測試。單元測試用於測試我的軟件的組件或方法,並不包括如數據庫、文件系統和網絡資源類的基礎結構測試。

固然咱們可使用編寫測試的最佳方法,如測試驅動開發(TDD)所指的先編寫單元測試,再編寫該單元測試要檢查的代碼,就比如先編寫書籍的大綱,再編寫書籍。其主要目的是爲了幫助開發人員編寫更簡單,更具可讀性的高效代碼。二者區別以下(來自Edison Zhou)

二、以深度(測試的細緻程度)和廣度(測試的覆蓋程度)區分, 測試分類以下(此處內容來自solenovex):

Unit Test 單元測試:它能夠測試一個類或者一個類的某個功能,但其覆蓋程度較低;

Integration Test 集成測試:它的細緻程度沒有單元測試高,可是有較好的覆蓋程度,它能夠測試功能的組合,以及像數據庫或文件系統這樣的外部資源;

Subcutaneous Test 皮下測試 :其做用區域爲UI層的下一層,有較好的覆蓋程度,可是深度欠佳;

UI測試:直接從UI層進行測試,覆蓋程度很高,可是深度欠佳

三、在編寫單元測試時,儘可能不要引入基礎結構依賴項,這些依賴項會下降測試速度,使測試更加脆弱,咱們應當將其保留供集成測試使用。能夠經過遵循顯示依賴項原則和使用依賴項注入避免應用程序中的這些依賴項,還能夠將單元測試保留在單獨的項目中與集成測試相分離,以確保單元測試項目沒有引用或依賴於基礎結構包。

總結下經常使用的單元測試和集成測試,單元測試會與外部資源隔離,以保證結果的一致性;而集成測試會依賴外部資源,且覆蓋面更廣。

二、測試的目的及特徵

一、爲何須要測試?咱們從以單元測試爲例從4個方面進行說明:

  • 時間人力成本:進行功能測試時,一般涉及打開應用程序,執行一系列須要遵循的步驟來驗證預期的行爲,這意味着測試人員須要瞭解這些步驟或聯繫熟悉該步驟的人來獲取結果。對於細微的更改或者是較大的更改,都須要重複上述過程,而單元測試只須要按一下按鈕便可運行,無需測試人員瞭解整個系統,測試結果也取決於測試運行程序而非測試人員。
  • 防止錯誤迴歸:程序更改後有時會出現舊功能異常的問題,因此測試時不只要測試新功能還要確保舊功能的正常運行。而單元測試能夠確保在更改一行代碼後從新運行整套測試,確保新代碼不會破壞現有的功能。
  • 可執行性:在給定某個輸入的狀況下,特定方法的做用或行爲可能不會很明顯。好比,輸入或傳遞空白字符串、null後,該方法會有怎樣的行爲?而當咱們使用一套命名正確的單元測試,並清楚的解釋給定的輸入和預期輸出,那麼它將能夠驗證其有效性。
  • 減小代碼耦合:當代碼緊密耦合時,會難以進行單元測試,因此以建立單元測試爲目的時,會在必定程度上要求咱們注意代碼的解耦

二、優質的測試須要符合哪些特徵,一樣以單元測試爲例:

  • 快速:成熟的項目會進行數千次的單元測試,因此應當花費很是少的時間來運行單元測試,通常來講在幾毫秒
  • 獨立:單元測試應當是獨立的,能夠單獨運行,不依賴文件系統或數據庫等外部因素
  • 可重複:單元測試的結果應當保持一致,即運行期間不進行更改,返回的結果應該相同
  • 自檢查:測試應當在沒有人工交互的狀況下,自動檢測是否經過
  • 及時:編寫單元測試不該該花費過多的時間,若是花費時間較長,應當考慮另一種更易測試的設計

在具體的執行時,咱們應當遵循一些最佳實踐規則,具體請參考微軟官方文檔單元測試最佳作法

三、xUnit框架介紹

經常使用的單元測試框架有MSTestxUnitNUnit,這裏咱們以xUnit爲例進行相關的說明

3.一、測試操做

首先咱們要明確如何編寫測試代碼,通常來講,測試分爲三個主要操做:

  • Arrange:意爲安排或準備,這裏能夠根據需求進行對象的建立或相關的設置;
  • Act:意爲操做,這裏能夠執行獲取生產代碼返回的結果或者是設置屬性;
  • Assert:意爲斷言,這裏能夠用來判斷某些項是否按預期進行,即測試經過仍是失敗

3.二、Assert類型

Assert時一般會對不一樣類型的返回值進行判斷,而在xUnit中是支持多種返回值類型的,經常使用的類型以下:

boolean:針對方法返回值爲bool的結果,能夠判斷結果是true或false

string:針對方法返回值爲string的結果,能夠判斷結果是否相等,是否以某字符串開頭或結尾,是否包含某些字符,並支持正則表達式

數值型:針對方法返回值爲數值的結果,能夠判斷數值是否相等,數值是否在某個區間內,數值是否爲null或非null

Collection:針對方法返回值爲集合的結果,能夠針對集合內全部元素或至少一個元素判斷其是否包含某某字符,兩個集合是否相等

ObjectType:針對方法返回值爲某種類型的狀況,能夠判斷是否爲預期的類型,一個類是否繼承於另外一個類,兩個類是否爲同一實例

Raised event:針對事件是否執行的狀況,能夠判斷方法內部是否執行了預期的事件

3.三、經常使用特性

在xUnit中還有一些經常使用的特性,可做用於方法或類,以下:

[Fact]:用來標註該方法爲測試方法

[Trait("Name","Value")]:用來對測試方法進行分組,支持標註多個不一樣的組名

[Fact(Skip="忽略說明...")]:用來修飾須要忽略測試的方法

3.4 、性能相關

在測試時咱們應當注意性能上的問題,針對一個對象供多個方法使用的狀況,咱們可使用共享上下文

  • 針對一個對象供同一類中的多個方法使用時,能夠將該對象提取出來,使用IClassFixture 對象將其注入到構造函數中
  • 針對一個對象供多個測試類使用的狀況,可使用ICollectionFixture 對象和[CollectionDefinition("...")]定義該對象

須要注意在使用IClassFixtureICollectionFixture對象時應當避免多個測試方法之間相互影響的狀況

3.五、數據驅動測試

在進行測試方法時,一般咱們會指定輸入值和輸出值,如但願多測試幾種狀況,咱們能夠定義多個測試方法,但這顯然不是一個最佳的實現;在合理的狀況下,咱們能夠將參數和數據分離,如何實現?

  • 方法一:使用[Theory]替換[Fact],將輸入輸出參數提取爲方法參數,並使用多個[InlineData("輸入參數","輸出參數)]來標註方法
  • 方法二:使用[Theory]替換[Fact],針對測試方法新增一個測試數據類,該類包含一個靜態屬性IEumerable<object[]>,將數據封裝爲一個list後賦值給該屬性,並使用[MemberData(nameof(數據類的屬性),MemberType=typeof(數據類))]標註測試方法便可;
  • 方法三:使用外部數據如數據庫數據/Excel數據/txt數據等,其實現原理與方法二相同,只是多了一個數據獲取封裝爲list的步驟;
  • 方法四:自定義一個Attribute,繼承自DataAttribute,實現其對應的方法,使用yield返回object類型的數組;使用時只須要在測試方法上方添加[Theory][自定義Attribute]便可

四、測試項目添加

4.一、添加測試項目

首先咱們右鍵項目解決方案選擇添加一個項目,輸入選擇xUnit後進行添加,項目命名爲BlogSystem.Core.Test,以下:

項目添加完成後咱們須要添加對測試項目的引用,在解決方案中右擊依賴項選擇添加BlogSystem.Core;這裏咱們預期對Controller進行測試,但後續有可能會添加其餘項目的測試,因此咱們創建一個Controller_Test文件夾保證項目結構相對清晰。

4.二、添加測試方法

在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.三、方案選擇

一、在進行測試時,咱們能夠根據實際狀況使用如下方案來進行測試:

  • 方案一:直接new一個Controller對象,調用其Action方法直接進行測試;適用於Controller沒有其餘依賴項的狀況;
  • 方案二:當有多個依賴項時,能夠藉助工具來模擬實例化時的依賴項,如Moq就是一個很好的工具;固然這須要必定的學習成本;
  • 方案三:模擬Http請求的方式來調用API進行測試;NuGet中的Microsoft.AspNetCore.TestHost就支持這類狀況;
  • 方案四:自定義方法實例化全部依賴項;將測試過程種須要用到的對象放到容器中並加載,其實現較爲複雜;

這裏咱們以測試UserController爲例,其構造函數包含了接口服務實例和HttpContext對象實例,Action方法內部又有數據庫鏈接操做,從嚴格意義上來說測試這類方法已經脫離了單元測試的範疇,屬於集成測試,但這類測試必定程度上能夠節省咱們大量的重複勞動。這裏咱們選擇方案三進行相關的測試。

二、如何使用TestHost對象?先來看看它的工做流程,首先它會建立一個IHostBuilder對象,並用它建立一個TestServer對象,TestServer對象能夠建立HttpClient對象,該對象支持發送及響應請求,以下圖所示(來自solenovex):

在嘗試使用該對象的過程當中咱們會發現一個問題,建立IHostBuilder對象時須要指明相似Startup的配置項,由於這裏是測試環境,因此實際上會與BlogSystem.Core中的配置類StartUp存在必定的差別,於是這裏咱們須要爲測試新創建一個Startup配置類。

4.四、方法實現

一、咱們在測試項目中添加名爲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();
        }
    }
}

二、這裏對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()
        {
            // 一、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

            // 二、Act
            var response = await host.GetTestClient().PostAsync($"http://localhost:5000/api/user/register", content);

            var result = await response.Content.ReadAsStringAsync();

            // 三、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.五、異常及解決

一、添加完上述的測試方法後,咱們使用打開Visual Studio自帶的測試資源管理器,點擊運行全部測試,發現提示錯誤沒法加載BLL?在原先的BlogSystem.Core的StartUp類中咱們是加載BLL和DAL項目的dll來達到解耦的目的,因此作了一個將dll輸出到Core項目bin文件夾的動做,可是在測試項目的TestStarup類中,咱們是沒法加載到BLL和DAL的。我嘗試將BLL和DAL同時輸出到兩個路徑下,但未找到對應的方法,因此這裏我採用了最簡單的解決方法,測試項目添加了對DAL和BLL的引用。再次運行,以下圖,彷佛成功了??

二、咱們在測試方法內部打上斷點,右擊測試方法,選擇調試測試,結果發現response參數爲空,只應Assert不嚴謹致使看上去沒有問題;在各類查找後,我終於找到了解決辦法,在TestStarup類的ConfigureServices方法內部service.AddControllers方法最後加上這麼一句話便可解決 .AddApplicationPart(Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, "BlogSystem.Core.dll")))

三、再次運行測試方法,成功!可是又發現了另一個問題,這裏咱們只是測試,可是數據庫中卻出現了咱們測試添加的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單元測試的藝術

聲明

相關文章
相關標籤/搜索