关于单元测试的思考--Asp.Net Core单元测试最佳实践

在我们码字过程中,单元测试是必不可少的。但在从业过程中,很多开发者却对单元测试望而却步。有些时候并不是不想写,而是常常会碰到下面这些问题,让开发者放下了码字的脚步:
  • 这个类初始数据太麻烦,你看:new MyService(new User("test",1), new MyDAO(new Connection(......)),new ToManyPropsClass(......) .....) 。我:。。。
  • 这个代码内部逻辑都是和Cookie有关,我单元测试不好整啊,还是得启动到浏览器里一个按钮一个按钮点。
  • 这个代码内部读了配置文件,单元测试也不能给我整个配置文件啊?
  • 这个代码主要是验证WebAPI入口得模型绑定,必须得调用一次啊?
  •     <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.1.0" />
        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
        <PackageReference Include="Moq" Version="4.8.3" />
        <PackageReference Include="NUnit" Version="3.9.0" />
        <PackageReference Include="NUnit3TestAdapter" Version="3.9.0" />
        <PackageReference Include="System.Linq" Version="4.3.0" />
    可以直接修改csproj文件,也可以nuget导入。测试的业务逻辑为:
    public class UserService{
            public bool CheckLogin(UserInfo user)
            {
                return user.Name == user.Password;  //登录逻辑,为了看着舒服,少点
            }
        }
    public class UserInfo{
            public string Name { get; set; }
            public string Password { get; set; }
        }
    测试的WebAPI控制器如下:
     public class ValuesController : ControllerBase
        {
            private UserService _service;
    
            public ValuesController(UserService service)
            {
                _service = service;
            }
    
            [HttpGet]
            [Route("checklogin")]
            public bool CheckLogin([FromQuery]UserInfo user)
            {
                return _service.CheckLogin(user);
            }
    
        }
    普通业务的单元测试
    public class TestService
        {
            private UserService _service;
    
            [SetUp]
            public void Init()
            {
                var server = new TestServer(WebHost.CreateDefaultBuilder().UseStartup<Startup>());
                _service = server.Host.Services.GetService<UserService>();
            }
            [Test]
            public void TestLogin()
            {
                bool result = _service.CheckLogin(new UserInfo { Name = "yubao", Password = "yubao" });
                Assert.IsTrue(result);
            }
        }
     在做业务测试过程中要善于使用注入功能,而不是使用new对象的方式,比如这里的Host.Services.GetService,防止出现new MyService(new User("test",1), new MyDAO(new Connection(......)),new ToManyPropsClass(......) .....)这种尴尬。用的越多你就越能体会这种做法的好处。我在openauth.net中使用的是autofac的AutofacServiceProvider。测试Controller很多时候我们需要测试顶层的controller(八成是controller里混的有业务逻辑)。这时我们可以快速的写出下面的测试代码:
     public class TestController
        {
            private ValuesController _controller;
    
            [SetUp]
            public void Init()
            {
                var server = new TestServer(WebHost.CreateDefaultBuilder().UseStartup<Startup>());
                _controller = server.Host.Services.GetService<ValuesController>();
            }
            [Test]
            public void TestLogin()
            {
                bool result = _controller.CheckLogin(new UserInfo{Name = "yubao",Password = "yubao"});
                Assert.IsTrue(result);
            }
        }
    这段代码在JAVA spring mvc框架下是没有问题的,但在asp.net core 中,你会发现:获取不到controller?spring mvc的理念就是万物皆服务,哪怕是一个controller也是一个普通的服务。但微软不喜欢这样,默认时它要掌控controller的生死(The Subtle Perils of Controller Dependency Injection in ASP.NET Core MVC 有人在声讨微软了)。所以我们不能通过普通的ServicCollection来注入和获取它,除非你指明Controller As Service,如下:
     public void ConfigureServices(IServiceCollection services)
            {
                services.AddMvc().AddControllersAsServices().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
            }
    这时即可顺利测试通过。测试含有HTTP上下文的业务逻辑,比如Cookie、URL中的QueryString 在平时的代码过程中,常常会和HTTP上下文HttpContext打交道,最常见的如request、response、cookie、querystring等,比如我们新的逻辑:
    public class UserService
        {
            private IHttpContextAccessor _httpContextAccessor;
    
            public UserService(IHttpContextAccessor httpContextAccessor)
            {
                _httpContextAccessor = httpContextAccessor;
            }
    
            public bool IsLogin()
            {
                return _httpContextAccessor.HttpContext.Request.Cookies["username"] != null;
            }
        }
    这时如何测试呢?马丁福勒在他的大作《企业应用架构模式》中明确指出“测试桩”的概念,来应对这种情况。各种Mock框架应运而生。比如我最喜欢的Moq:
    public class TestCookie
        {
            private UserService _service;
    
            [SetUp]
            public void Init()
            {
                var httpContextAccessorMock = new Mock<IHttpContextAccessor>();
                httpContextAccessorMock.Setup(x => x.HttpContext.Request.Cookies["username"]).Returns("yubaolee");
    
                var server = new TestServer(WebHost.CreateDefaultBuilder()
                    .ConfigureServices(u =>u.AddScoped(x =>httpContextAccessorMock.Object))
                    .UseStartup<Startup>());
                _service = server.Host.Services.GetService<UserService>();
            }
            [Test]
            public void TestLogin()
            {
                bool result = _service.IsLogin();
                Assert.IsTrue(result);
            }
        }
      测试一次HTTP请求 有时我们需要测试Mvc框架的模型绑定,看看一次客户端的请求是否能被正确解析,亦或者测试WebAPI入口的一些Filter AOP等是否被正确触发,这时就需要测试一次HTTP请求。从严格意义上来讲这种测试已经脱离的单元测试的范畴,属于集成测试。但这种测试代码可以节省我们大量的重复劳动。asp.net core中可以通过TestServer快速实现这种模拟:
    public class TestHttpRequest
        {
            private TestServer _testServer;
    
            [SetUp]
            public void Init()
            {
                _testServer = new TestServer(WebHost.CreateDefaultBuilder().UseStartup<Startup>());
            }
            [Test]
            public void TestLogin()
            {
                var client = _testServer.CreateClient();
                var result = client.GetStringAsync("/api/values/checklogin?name=yubao&password=yubao");
                Console.WriteLine(result.Result);
            }
        }
    在进行单元测试的过程中,测试的理念(或者TDD的思维?)异常重要,它能帮助你构建和谐优美的代码。

    G

    相关内容推荐