使用Microsoft.AspNetCore.TestHost進行完整的功能測試


簡介

Microsoft.AspNetCore.TestHost是可以用於Asp.net Core 的功能測試工具。很多時候我們一個接口寫好了,單元測試什么的也都ok了,需要完整調試一下,檢查下單元測試未覆蓋到的代碼是否有bug。步驟為如下:程序打個斷點->F5運行->通常需要登錄個測試賬號->查找要調試api的入口->獲得斷點開始調試=>代碼報錯?很多時候需要停止調試修改->回到第一步。如此反復循環,做着重復的工作,Microsoft.AspNetCore.TestHost正是為了解決這個問題,它可以讓你使用xTest或者MSTest進行覆蓋整個HTTP請求生命周期的功能測試。

進行一個簡單的功能測試

新建一個Asp.net Core WebApi和xUnit項目

ValuesController里面自帶一個Action

我們在xUnit項目里面模擬訪問這個接口,首選安裝如下nuget包:

  • Microsoft.AspNetCore.TestHost
  • Microsoft.AspNetCore.All(很多依賴懶得找的話直接安裝這個集成包,百分之90涉及到AspNetCore的依賴都包含在里面)

然后需要引用被測試的AspnetCoreFunctionalTestDemo項目,新建一個測試類ValuesControllerTest

將GetValuesTest方法替換為如下代碼,其中startup類是應用自AspnetCoreFunctionalTestDemo項目

        [Fact]
        public void GetValuesTest()
        {

            var client = new TestServer(WebHost
                .CreateDefaultBuilder()
                .UseStartup<Startup>())
                .CreateClient();

            string result = client.GetStringAsync("api/values").Result;

            Assert.Equal(result, JsonConvert.SerializeObject(new string[] { "value1", "value2" }));
        }

此時在ValueController打下斷點

 

運行GetValuesTest調試測試

成功進入斷點,我們不用啟動瀏覽器,就可以進行完整的接口功能測試了。

修改內容目錄與自動授權

上面演示了如何進行一個簡單的功能測試,但是存在兩個缺陷:

  1. webApi在測試的時候實際的運行目錄是在FunctionalTest目錄下
  2. 對需要授權的接口不能正常測試,會得到未授權的返回結果

 1.內容目錄

我們可以在Controller的Get方法輸出當前的內容目錄

內容目錄是在測試x項目下這與我們的預期不符,如果webapi項目對根目錄下的文件有依賴關系例如appsetting.json則會找不到該文件,解決的辦法是在webHost中手動指定運行根目錄

[Fact]
public void GetValuesTest()
{

    var client = new TestServer(WebHost
        .CreateDefaultBuilder()
        .UseContentRoot(GetProjectPath("AspnetCoreFunctionalTestDemo.sln", "", typeof(Startup).Assembly))
        .UseStartup<Startup>())
        .CreateClient();

    string result = client.GetStringAsync("api/values").Result;

    Assert.Equal(result, JsonConvert.SerializeObject(new string[] { "value1", "value2" }));
}

/// <summary>
/// 獲取工程路徑
/// </summary>
/// <param name="slnName">解決方案文件名,例test.sln</param>
/// <param name="solutionRelativePath">如果項目與解決方案文件不在一個目錄,例如src文件夾中,則傳src</param>
/// <param name="startupAssembly">程序集</param>
/// <returns></returns>
private static string GetProjectPath(string slnName, string solutionRelativePath, Assembly startupAssembly)
{
      string projectName = startupAssembly.GetName().Name;
      string applicationBasePath = PlatformServices.Default.Application.ApplicationBasePath;
      var directoryInfo = new DirectoryInfo(applicationBasePath);
      do
      {
          var solutionFileInfo = new FileInfo(Path.Combine(directoryInfo.FullName, slnName));
          if (solutionFileInfo.Exists)
          {
              return Path.GetFullPath(Path.Combine(directoryInfo.FullName, solutionRelativePath, projectName));
          }

          directoryInfo = directoryInfo.Parent;
      }
      while (directoryInfo.Parent != null);

      throw new Exception($"Solution root could not be located using application root {applicationBasePath}.");
}

 GetProjectPath方法采用遞歸的方式找到startup的項目所在路徑,此時我們再運行

2.自動授權

每次測試時手動登錄這是一件很煩人的事情,所以我們希望可以自動話,這里演示的時cookie方式的自動授權

首先在startup文件配置cookie認證

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Authentication.Cookies;

namespace AspnetCoreFunctionalTestDemo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.AddAuthentication(o => o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(o => { o.ExpireTimeSpan = new TimeSpan(0, 0, 30); o.Events.OnRedirectToLogin = (context) => { context.Response.StatusCode = 401; return Task.CompletedTask; }; });         }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
           app.UseAuthentication();
            app.UseMvc();
        }
    }
}

這里覆蓋了cookie認證失敗的默認操作改為返回401狀態碼。

在valuesController新增登錄的Action並配置Get的Action需要授權訪問

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Security.Claims;

namespace AspnetCoreFunctionalTestDemo.Controllers
{
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        // GET api/values
        [HttpGet,Authorize]
        public IEnumerable<string> Get([FromServices]IHostingEnvironment env)
        {
            return new string[] { "value1", "value2" };
        }

        // POST api/values
        [HttpGet("Login")] public void Login() { var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme); identity.AddClaim(new Claim(ClaimTypes.Name, "huanent")); var principal = new ClaimsPrincipal(identity); HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal).Wait(); }
    }
}

此時我們使用測試項目測試Get方法

如我們預期,返回了401,說明未授權。我們修改下GetValuesTest

using AspnetCoreFunctionalTestDemo;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.PlatformAbstractions;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using static Microsoft.AspNetCore.WebSockets.Internal.Constants;

namespace FunctionalTest
{
    public class ValuesControllerTest
    {

        [Fact]
        public void GetValuesTest()
        {
            var client = new TestServer(
                WebHost.CreateDefaultBuilder()
                       .UseStartup<Startup>()
                       .UseContentRoot(GetProjectPath("AspnetCoreFunctionalTestDemo.sln", "", typeof(Startup).Assembly))
                       ).CreateClient();
            var respone = client.GetAsync("api/values/login").Result; SetCookie(client, respone); var result = client.GetAsync("api/values").Result;
        }

        private static void SetCookie(HttpClient client, HttpResponseMessage respone) { string cookieString = respone.Headers.GetValues("Set-Cookie").First(); string cookieBody = cookieString.Split(';').First(); client.DefaultRequestHeaders.Add("Cookie", cookieBody); } /// <summary>
        /// 獲取工程路徑
        /// </summary>
        /// <param name="slnName">解決方案文件名,例test.sln</param>
        /// <param name="solutionRelativePath">如果項目與解決方案文件不在一個目錄,例如src文件夾中,則傳src</param>
        /// <param name="startupAssembly">程序集</param>
        /// <returns></returns>
        private static string GetProjectPath(string slnName, string solutionRelativePath, Assembly startupAssembly)
        {
            string projectName = startupAssembly.GetName().Name;
            string applicationBasePath = PlatformServices.Default.Application.ApplicationBasePath;
            var directoryInfo = new DirectoryInfo(applicationBasePath);
            do
            {
                var solutionFileInfo = new FileInfo(Path.Combine(directoryInfo.FullName, slnName));
                if (solutionFileInfo.Exists)
                {
                    return Path.GetFullPath(Path.Combine(directoryInfo.FullName, solutionRelativePath, projectName));
                }

                directoryInfo = directoryInfo.Parent;
            }
            while (directoryInfo.Parent != null);

            throw new Exception($"Solution root could not be located using application root {applicationBasePath}.");
        }
    }
}

我們首先訪問api/Values/Login,獲取到Cookie,然后講cookie附在httpclient的默認http頭上,這樣就能夠成功訪問需要授權的接口了

總結

通過上面演示,我們已經可以很大程度地模擬了整個api請求,讓我們可以方便地一鍵調試目標接口,再也不用開瀏覽器或postman了。

附上演示項目地址:https://github.com/huanent/AspnetCoreFunctionalTestDemo


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM