.netcore持续集成测试篇之测试方法改造

系列目录html

经过前面两节讲解,咱们的测试类中已经有两个测试方法了,整体上以下web

public class mvc20
    {
        private readonly HttpClient _client;

        public mvc20()
        {
            var builder = new WebHostBuilder()
                .UseContentRoot(@"E:\personal project\newTest2018\ConsoleApp1\CoreMvc")
                .UseEnvironment("Development")
                .UseStartup<CoreMvc.Startup>();
            var server = new TestServer(builder);
             _client = server.CreateClient();
        }
        [Fact]
        public async Task SimpleGet()
        {
            var response = await _client.GetAsync("/HelloWorld/Hello");
            response.EnsureSuccessStatusCode();
            var responseStr = await response.Content.ReadAsStringAsync();
            Assert.Equal("Hello,World", responseStr);
        }

        [Theory]
        [AutoData]
        public async Task SimplePost(Student stud)
        {
            var content = new StringContent(JsonConvert.SerializeObject(stud), Encoding.UTF8, "application/json");
            var response = await _client.PostAsync("/HelloWorld/StudentInfo", content);
            response.EnsureSuccessStatusCode();
            var result = await response.Content.ReadAsStringAsync();
            Assert.True(!string.IsNullOrEmpty(result));
        }
    }

改进一:将对象初始化移到外部类中

以上方法看似没有问题,实际上却有一个性能陷阱,咱们经过前面章节的知识已经知道,xunit里测试类的构造函数会在每个测试方法运行的时候都执行一遍,一般状况下咱们的测试代码远不止三几个,有时候几十个甚至上百个.这样每次都建立一个是很是影响性能的.而且这里的TestServer和_client都没有释放.此外就是web项目里可能每个测试类都须要建立这样一个TestServer,这样重复的代码会复制不少次,带来维护困难.sql

咱们前面讲到过,咱们若是想要让一个对象在一个测试类中只初始化一次,就要让这个类实现IClassFixture泛型接口,类在初始化的时候会自动注入这个泛型对象的实体,而且只初始化一次,若是这个泛型对象实现了IDisposable接口,则会在测试类全部方法都执行完成的时候执行这个对象里的Dispose方法.json

首先咱们建立一个名为MyTestServerFixtrue的类,TestServer和HttpClient对象的初始化在这里执行.代码以下服务器

public class MyTestServerFixtrue:IDisposable
    {
        public readonly HttpClient _client;
        private readonly TestServer _server;
        public MyTestServerFixtrue()
        {
            var builder = new WebHostBuilder()
                .UseContentRoot(@"E:\personal project\newTest2018\ConsoleApp1\CoreMvc")
                .UseEnvironment("Development")
                .UseStartup<CoreMvc.Startup>();
             _server = new TestServer(builder);
            _client = _server.CreateClient();
        }

        public void Dispose()
        {
            _client.Dispose();
            _server.Dispose();
        }

这里的方法和参数大部分都和前面在测试类中添加的同样,只是有如下几点须要注意:
1.把server变量放在构造函数外边,这样咱们才能在Dispose里把它释放掉,否则没法定位到它.
2.把client变成public类型,由于咱们须要在测试类中访问它.mvc

下面咱们再看测试类改造后的代码app

public class mvc20:IClassFixture<MyTestServerFixtrue>
    {
        private readonly HttpClient _client;

        public mvc20(MyTestServerFixtrue fixtrue)
        {
            this._client = fixtrue._client;
        }
    }

这里是主要代码,首先这个实现了IClassFixture,而后咱们把无参构造函数改变成有参的,而且传入MyTestServerFixtrue类型对象,Xunit会自动注入这个对象,而后咱们把这个对象里的httpclient赋值给本类的_client对象,这样咱们就能够在本类中使用它了.异步

这样其它的测试类也能够实现IClassFixture<MyTestServerFixtrue>,若是想要改TestServer的配置只须要在MyTestServerFixtrue类中改就好了.async

改进二:固定路由参数

咱们看到前面讲到的两个测试方法提交的路径中都包含"/HelloWorld",它其实匹配控制器名,通常状况下同一个Controller下的方法的测试方法都写在同一个测试类中.这样Controller名称是固定的,咱们能够把它单独抽离出来,只须要Action后面的路由.函数

咱们把测试类改为以下:

public class mvc20:IClassFixture<MyTestServerFixtrue>
    {
        private readonly HttpClient _client;

        public mvc20(MyTestServerFixtrue fixtrue)
        {
            var baseAddr = fixtrue._client.BaseAddress.AbsoluteUri;
            string controllerName ="HelloWorld";
            this._client = fixtrue._client;
            if (!fixtrue._client.BaseAddress.AbsoluteUri.Contains(controllerName))
            {
                fixtrue._client.BaseAddress = new Uri(baseAddr + controllerName+"/");
            }
        }
        [Fact]
        public async Task SimpleGet()
        {
            var response = await _client.GetAsync($"{nameof(HelloWorldController.Hello)}");
            response.EnsureSuccessStatusCode();
            var responseStr = await response.Content.ReadAsStringAsync();
            Assert.Equal("Hello,World", responseStr);
        }

        [Theory]
        [AutoData]
        public async Task SimplePost(Student stud)
        {
            var content = new StringContent(JsonConvert.SerializeObject(stud), Encoding.UTF8, "application/json");
            var response = await _client.PostAsync($"{nameof(HelloWorldController.StudentInfo)}", content);
            response.EnsureSuccessStatusCode();
            var result = await response.Content.ReadAsStringAsync();
            Assert.True(!string.IsNullOrEmpty(result));
        }
    }

这里咱们把controller的名称加到HttpClient的BaseUrl里面,而后发送get,post等请求的时候只要Action的名字,这里咱们使用nameof关键字来获取action的名字,使用nameof关键字来获取的好处是:第一,咱们点击方法名就能够快速定位到指定的方法.更为重要的是若是方法的名称改了,编译的时候就会出现编译错误,咱们能够快速定位到错误而后修改.

改进三:资源路径改成相对路径

上面MyTestServerFixtrue类中的代码有一处有明显问题:那就是UseContentRoot里的路径是写死的,项目在本机上地址与在服务器上的或者与其它同事的绝大多数状况下是不同的(由于你们项目所在的目录名不相同)这时候若是其它人调用这些代码就可能会出现错误.

咱们可使用相对路径来获取绝对路来解决这个问题,因为这两个项目的主文件夹在同一文件夹下面,所以测试项目向外退若干层就可以获得mvc项目的主目录了.

咱们将MyTestServerFixtrue类的构造方法改成以下:

public MyTestServerFixtrue()
        {
            var rootPath = GetContentRootDir();
            var builder = new WebHostBuilder()
                .UseContentRoot(rootPath)
                .UseEnvironment("Development")
                .UseStartup<CoreMvc.Startup>();
             _server = new TestServer(builder);
            _client = _server.CreateClient();
        }

此次咱们不是再写死rootPath而是经过方法GetContentRootDir来获取.
下面咱们来看这个GetContentRootDir方法

private string GetContentRootDir()
        {
            var currentPath = AppDomain.CurrentDomain.BaseDirectory;
            var relativePath = @"..\..\..\..\CoreMvc";
            var combinedPath = Path.Combine(currentPath, relativePath);
            var absPath = Path.GetFullPath(combinedPath);
            return absPath;
        }

首先咱们先获取当前程序域的目录,也就是程序的运行目录,获取到它以后咱们看看向上移动多少层可以到达包含mvc项目和这个test项目的文件夹,经查是四层,下面的相对路径咱们就写为如变量relativePath定义的那样.
咱们把它们组合在一块儿,而后经过Path.GetFullPath来获取到相对路径的绝路径.

改进四 设置超时

有时候服务器故障会致使请求很是慢,服务器很长时间没法返回请求,这就会致使集成测试代码一直'卡'着没法完成,这时候能够设置一个超时.设置很是简单,HttpClient有一个Timeout属性,设置相应的超时时间便可.HttpClient的默认请求超时时间是100s,这个值应该大部分时候不须要修改的,可是关于具体的业务,可能有一些方法自己执行时间特别长(业务逻辑很是复杂,sql语句很是复杂等)这时候能够单元给本次请求设置一个超时时间.好比说是150s,设置以下

CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(150));
            var response = await client.GetAsync("/Home/index", cts.Token);

这里定义一个CancellationTokenSource对象,并指定超时时间,而后把此对象的Token对象传给异步请求方法.