使用Microsoft.AspNetCore.TestHost进行完整的功能测试

发表于:2017-11-24 16:03

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:huanent    来源:51Testing软件测试网采编

  简介
  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调试测试
  成功进入断点,我们不用启动浏览器,就可以进行完整的接口功能测试了。
  修改内容目录与自动授权
  上面演示了如何进行一个简单的功能测试,但是存在两个缺陷:
  webApi在测试的时候实际的运行目录是在FunctionalTest目录下
  对需要授权的接口不能正常测试,会得到未授权的返回结果
  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
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号