目录

介绍

概念

设置

网络API服务

演示集成测试DLL

Clifton.IntegrationTestWorkflowEngine DLL

工作流测试方法DLL

我们的第一个Fluent集成测试

重构WorkflowPacket类

重构ApiMethods类

添加端点调用

测试结果

重构端点

测试结果

审查

Real Endpoint Fluent集成测试

添加通用Get REST API调用

添加工作流API调用方法

审查

故障测试

使用Dynamic减少通用参数规范

更进一步——POST和使用JSON Body

一个简单的内存中州县模型

状态控制器

新的Fluent API端点方法

集成测试

运行测试

审查

Fluent的缺点

结论——什么是模式?


  • 下载源 - 241.9 KB

介绍

对于我所做的这类工作,Web API集成测试不仅仅是调API 并验证我得到预期结果的简单问题,它实际上是一个工作流程。例如,不是设置所有先决条件数据来进行单个API测试,而是使用API本身来帮助设置数据。此外,然后测试用户的工作流,这需要按顺序调用多个端点。好处是:

  1. 它简化了测试设置过程。
  2. 它更接近地模拟用户可能会做什么或前端应用程序为用户做什么。
  3. 它审查API是否真的支持原子行为,如果你愿意的话,而不是一个控制器做十几种不同的事情。
    1. 是的,您可能仍然有执行复杂操作的端点,但重点是这些端点应该基于更简单的API端点可以调用的更多“原子”方法。
  4. 如果使用模型,它往往会强制实现一种架构,即模型在单独的程序集中维护,该程序集可以在服务实现和集成测试应用程序之间共享。
  5. 该概念与Fluent Assertions很好地集成在一起,我们将在此处使用它。

概念

这个概念非常简单:

  1. 我们有一个集成测试套件(实际上是使用单元测试框架实现的)...
  2. ...调用我们“fluent”库中的方法...
  3. ...调用所需的端点...
  4. ...我们可以捕获结果。

最后一部分“我们可以捕获结果”是有趣的部分,因为我们想要捕获:

  1. 生成的HTTP状态代码和文本。
  2. 返回的JSON(是的,我假设一切都是JSON)。
  3. 如果调用中没有错误,则反序列化JSON。
  4. 将反序列化的对象与我们稍后可以用来引用它的标签相关联。

这要求我们为管理上述信息的测试工作流实现一个包装类。我一直无法为此想出一个好名字,因此我将其简单地称为“工作流数据包”。

设置

网络API服务

我们将创建一个新的VS 2019项目:

并选择:

我将把这个项目称为“FluentWebApiIntegrationTestDemo” 。

Visual Studio 2019为Web API创建基本模板,包括示例天气预报控制器:

我要重命名和改名,所以它看起来很简单:

using System;using Microsoft.AspNetCore.Mvc;namespace FluentWebApiIntegrationTestDemo.Controllers
{[ApiController][Route("[controller]")]public class DemoController : ControllerBase{[HttpGet]public object Get(){throw new NotImplementedException();}}
}

并删除WeatherForecast.cs文件。

为了在浏览器中进行测试,调试配置将在控制器名称上打开浏览器:

并将启动IIS,因此我不必处理端口的愚蠢:

然后我们可以运行这个项目(VS会第一次提供IIS,这很棒),我们看到:

很好!

演示集成测试DLL

接下来,我将添加一个MSTest测试项目(.NET Core)——是的,我正在使用单元测试框架来执行集成测试。

创建集成测试项目导致了错误的噩梦:

我发现的唯一“解决方案”是将Web API项目和集成测试项目并排放置:

无论Visual Studio对在与Web Core API项目相同的文件夹中创建的项目做什么......好吧,它做得太多了,因为在我看来,文件夹结构不应该与Web Core API项目的方式有任何关系建成。

我还升级了软件包:

到:

所以最终建立。

VS创建的初始UnitTest1.cs文件,我已重命名为“DemoTests”,此存根如下所示:

using Microsoft.VisualStudio.TestTools.UnitTesting;namespace IntegrationTests
{[TestClass]public class DemoTests{[TestMethod]public void GetTest(){}}
}

当然,测试并没有做任何事情,所以我们测试成功了!对不起皮特。

Clifton.IntegrationTestWorkflowEngine DLL

因为这些集成测试实际上可以用作“实时”工作流,所以我将创建一个单独的.NET Core类库项目来管理fluent集成工作流数据包:

目前只包含一个存根文件:

namespace Clifton.IntegrationTestWorkflowEngine
{public class WorkflowPacket{}
}

创建一个只有一个类的DLL似乎有点矫枉过正,但我​​们可能希望稍后添加其他功能。这里的重点是我们想要一个不允许我们添加“特定于域”的实现的可重用类,因此我们创建了一个单独的DLL来防止这种情况。

工作流测试方法DLL

如果以上还不够,是的,我们将再创建一个.NET Core类库DLL来实际包含工作流方法。如果我们选择向用户公开工作流,这将允许我们直接在API中使用工作流方法。不妨提前计划,而不是稍后重构。此DL特定于域的——它将包含调用我们演示API服务中的端点的方法。

它还有一个存根类:

namespace WorkflowTestMethods
{public class ApiMethods{}
}

我们的第一个Fluent集成测试

设置几个项目引用后,我们可以编写第一个流畅的集成测试:

string baseUrl = "<a href="http://localhost/FluentWebApiIntegrationtestDemo">http://localhost/FluentWebApiIntegrationtestDemo</a>";new WorkflowPacket(baseUrl).Home("Demo").IShouldSeeOKResponse();

这不会编译,因为我们还没有实现接受基本URL和支持方法的构造函数,但是从语法可以收集:

  1. fluent方法是对WorkflowPacket的扩展
  2. 每个fluent方法都返回WorkflowPacket实例。

重构WorkflowPacket类

重构WorkflowPacket类:

using System.Net;namespace Clifton.IntegrationTestWorkflowEngine
{public class WorkflowPacket{public HttpStatusCode LastResponse { get; set; }public string BaseUrl { get; protected set; }public WorkflowPacket(string baseUrl){this.BaseUrl = baseUrl;}}
}

重构ApiMethods类

重构ApiMethods(我也添加了FluentAssertions包)类:

using FluentAssertions;using Clifton.IntegrationTestWorkflowEngine;
using System.Net;namespace WorkflowTestMethods
{public static class ApiMethods{public static WorkflowPacket Home(this WorkflowPacket wp, string controller){return wp; }public static WorkflowPacket IShouldSeeOKResponse(this WorkflowPacket wp){wp.LastResponse.Should().Be(HttpStatusCode.OK, $"Did not expected {wp.LastContent}");return wp;}public static WorkflowPacket IShouldSeeNoContentResponse(this WorkflowPacket wp){wp.LastResponse.Should().Be(HttpStatusCode.NoContent, $"Did not expected {wp.LastContent}");return wp;}public static WorkflowPacket IShouldSeeBadRequestResponse(this WorkflowPacket wp){wp.LastResponse.Should().Be(HttpStatusCode.BadRequest, $"Did not expected {wp.LastContent}");return wp;}}
}

现在,FluentAssertions有点蹩脚。可以说“我们断言x应该是y是因为[某些原因]”,但是没有机制可以说“我们断言x应该是 y,但这不是因为[失败原因]。所以这就是为什么“because”参数有“Did not expected...”。唉。

测试结果

我们看到集成测试失败了(显然,因为我们还没有调用端点):

FluentAssertions令人惊奇的是,它可以准确地告诉您问题所在:

添加端点调用

使用RestSharp(又一个包),我将把它包装在一个RestService类中并放入Clifton.IntegrationTestWorkflowEngine(哈!看,我告诉过你我们会向这个DLL添加更多内容!),我们有一个简单的GET API调用方法:

using System.Net;using RestSharp;namespace Clifton.IntegrationTestWorkflowEngine
{public static class RestService{public static (HttpStatusCode status, string content) Get(string url){var response = Execute(url, Method.GET);return (response.StatusCode, response.Content);}private static IRestResponse Execute(string url, Method method){var client = new RestClient(url);var request = new RestRequest(method);var response = client.Execute(request);return response;}}
}

然后我们重构ApiMethods.Home方法来进行调用:

public static WorkflowPacket Home(this WorkflowPacket wp, string controller)
{var resp = RestService.Get($"{wp.BaseUrl}/{controller}");wp.LastResponse = resp.status;return wp;
}

测试结果

测试仍然失败,但现在我们明白了原因:

重构端点

所以最后一步是重构端点,这样它就不会抛出NotImplementedException异常而是返回OK:

[HttpGet]
public object Get()
{return Ok();
}

测试结果

终于测试通过了!

审查

我们完成了什么?鉴于这个简单的例子:

new WorkflowPacket(baseUrl).Home("Demo").IShouldSeeOKResponse();

我们创建了以下基本框架:

  1. 调用端点
  2. 验证状态返回

现在让我们进一步扩展它以使用更多“真实”的API端点。

Real Endpoint Fluent集成测试

这里的目的是为了能够将一些数据传入某个端点(查询字符串或序列化)并获取结果(反序列化)并测试结果。因此,例如:

[TestMethod]
public void FactorialTest()
{string baseUrl = "http://localhost/FluentWebApiIntegrationtestDemo";new WorkflowPacket(baseUrl).Factorial<FactorialResult>("factResult", 6).IShouldSeeOKResponse().ThenIShouldSee<FactorialResult>("factResult", r => r.Result.Should().Be(720));
}[TestMethod]
public void BadFactorialTest()
{string baseUrl = "http://localhost/FluentWebApiIntegrationtestDemo";new WorkflowPacket(baseUrl).Factorial<FactorialResult>("factResult", -1).IShouldSeeBadRequestResponse();
}

请注意,这意味着工作流数据包现在将结果存储在指示的“容器”中,在本例中为“factResult”。

另请注意,我希望在第二个测试中,如果我尝试获取小于1的数字的阶乘,将返回的HTTP BadRequest响应。

我将把FactorialResult “模型”放到另一个由集成测试和API服务共享的DLL中:

namespace FluentWebApiIntegrationTestDemoModels
{public class FactorialResult{public decimal Result { get; set; }}
}

因为这些是通用方法,所以我们不会将此DLL添加到WorkflowTestMethods项目中。

添加通用Get REST API调用

添加Newtonsoft.Json包,我们实现:

public static (T item, HttpStatusCode status, string content) Get<T>(string url) where T : new()
{var response = Execute(url, Method.GET);T ret = TryDeserialize<T>(response);return (ret, response.StatusCode, response.Content);
}private static T TryDeserialize<T>(IRestResponse response) where T : new()
{T ret = new T();int code = (int)response.StatusCode;if (code >= 200 && code < 300){ret = JsonConvert.DeserializeObject<T>(response.Content);}return ret;
}

添加工作流API调用方法

在这里,在Math控制器中Factorial方法的端点是硬编码的,我认为这很好,因为API调用方法的描述应该是具体的,以便集成测试可以通过其方法名称而不是其参数来读取。

public static WorkflowPacket Factorial<T>
(this WorkflowPacket wp, string containerName, int n) where T: new()
{var resp = RestService.Get<T>($"{wp.BaseUrl}/Math/Factorial?n={n}");wp.LastResponse = resp.status;wp.Container[containerName] = resp.item;return wp;
}public static WorkflowPacket ThenIShouldSee<T>
(this WorkflowPacket wp, string containerName, Action<T> test) where T : class
{T obj = wp.GetObject<T>(containerName);test(obj);return wp;
}

请注意,我现在已将容器的概念添加到WorkflowPacket中,这样我就可以向container中添加对象并返回container对象,并将其转换为指定的类型。

public Dictionary<string, object> Container = new Dictionary<string, object>();
...
public T GetObject<T>(string containerName) where T: class
{Container.Should().ContainKey(containerName);T ret = Container[containerName] as T;return ret;
}

当然,测试失败是因为我还没有使用该Factorial方法实现该Math控制器,所以现在让我们将其作为存根执行:

[ApiController]
[Route("[controller]")]
public class MathController : ControllerBase
{[HttpGet("Factorial")]public object Factorial([FromQuery, BindRequired] int n){return Ok(new FactorialResult());}
}

同样,测试失败,但我们看到:

所以让我们实际实现阶乘计算:

[ApiController]
[Route("[controller]")]
public class MathController : ControllerBase
{[HttpGet("Factorial")]public object Factorial([FromQuery, BindRequired] int n){object ret;if (n <= 0){ret = BadRequest("Value must be >= 1");}else{decimal factorial = 1;n.ForEach(i => factorial = factorial * i, 1);ret = Ok(new FactorialResult() { Result = factorial });}return ret;}
}

(是的,我喜欢我的扩展方法。)请注意我是如何专门编写一个测试以确保n大于0。

我们看到:

审查

鉴于此集成测试:

new WorkflowPacket(baseUrl).Factorial<FactorialResult>("factResult", 6).IShouldSeeOKResponse().ThenIShouldSee<FactorialResult>("factResult", r => r.Result.Should().Be(720));

除非我们不幸不得不过度指定泛型,否则我们可以:

  1. 使用查询参数进行API端点调用。
  2. 反序列化结果。
  3. 验证结果。

故障测试

我们还实现了一个简单的集成测试,以验证API是否通过简单的工作流程优雅地处理错误输入:

new WorkflowPacket(baseUrl).Factorial<FactorialResult>("factResult", -1).IShouldSeeBadRequestResponse();

使用Dynamic减少通用参数规范

如果我们想使用C#的动态特性(虽然我们失去了智能感知),我们可以使用稍微不同的工作流方法,编写:

.ThenIShouldSee("factResult", r => r.Result.Should().Be(720));

除了我们从运行时绑定器中得到一个异常:

我们可以实现这样的动态ThenIShouldSee:

public static WorkflowPacket ThenIShouldSee
(this WorkflowPacket wp, string containerName, Func<dynamic, bool> test)
{var obj = wp.GetObject(containerName);var b = test(obj);b.Should().BeTrue();return wp;
}

测试写为:

.ThenIShouldSee("factResult", r => r.Result == 720M);

但接下来看看会发生什么:

什么?事实证明var b,即使我们和编译器都知道b是类型bool,但其与FluentAssertions也不能很好的一起工作。我们实际上必须为FluentAssertions编写bool b!

更进一步——POST和使用JSON Body

在集成测试中更典型的是模拟用户可能执行的几个活动。对于此示例,我们将创建一些测试,如果有UI,将允许用户为每个州输入州和县,并按州查看县。一组简单的端点,我将直接在内存中实现——我甚至不会使用内存数据库!诚然,这是一个有点人为的例子,但它说明了更有趣的集成测试。

一个简单的内存中州县模型

我将不再使用测试驱动开发(TDD),而是在编写相当简单的代码时做对我来说感觉更自然的事情——只需编写实现,然后编写测试来验证实现。我称之为稍后测试编码——TLC,哈哈哈。这是模型,请注意我如何在模型中编码特定异常:

namespace FluentWebApiIntegrationTestDemoModels
{public class StateModelException : Exception{public StateModelException() { } public StateModelException(string msg) : base(msg) { }}public class County : List<string> { }public class StateModel{// Public for serializationpublic Dictionary<string, County> StateCounties { get; set; } = new Dictionary<string, County>();public IEnumerable<string> GetStates(){var ret = StateCounties.Select(kvp => kvp.Key);return ret;}public IEnumerable<string> GetCounties(string stateName){Assertion.That<StateModelException>(StateCounties.ContainsKey(stateName), "State does not exist.");return StateCounties[stateName];}public void AddState(string stateName){Assertion.That<StateModelException>(!StateCounties.ContainsKey(stateName), "State already exists.");StateCounties[stateName] = new County();}public void AddCounty(string stateName, string countyName){Assertion.That<StateModelException>(StateCounties.ContainsKey(stateName), "State does not exists.");Assertion.That<StateModelException>(!StateCounties[stateName].Contains(countyName), "County already exists.");StateCounties[stateName].Add(countyName);}}
}

我们想要断言模型中的预期条件,而不是控制器,以便模型可以在所有验证中重新使用。

状态控制器

这是控制器:

[ApiController]
[Route("[controller]")]
public class StateController : ControllerBase
{public static StateModel stateModel  = new StateModel();[HttpGet("")]public object GetStates(){var states = stateModel.GetStates();return Ok(states);}[HttpPost("")]public object AddState([FromBody] string stateName){object ret = Try<StateModelException>(NoContent(), () => stateModel.AddState(stateName));return ret;}[HttpPost("{stateName}/County")]public object AddCounty([FromRoute, BindRequired] string stateName,[FromBody] string countyName){object ret = Try<StateModelException>(NoContent(), () => stateModel.AddCounty(stateName, countyName));return ret;}
}

因为我真的不喜欢重复自己,我也不喜欢用try-catch块和if-else语句弄乱我的代码,如果可能的话,我创建了一个辅助函数,如果我们从模型中看到预期的异常,然后返回一个错误的请求,否则抛出异常并让框架返回内部服务器错误。这段代码说明了大多数API方法应该真正做非常简单的事情,这些事情有一组有限的异常。虽然更复杂的API端点可能会抛出各种异常,但我在这里提出的更多是作为谈话/讨论点而不是作为指导。对我来说,当我教授软件开发/架构时,重点是让人们思考他们应该问的问题,而不仅仅是深入研究机器人编码。

private object Try<T>(object defaultReturn, Action action)
{object ret = defaultReturn;try{action();}catch (Exception ex){if (ex.GetType().Name == typeof(T).Name){ret = BadRequest(ex.Message);}else{throw;}}return ret;
}

新的Fluent API端点方法

我添加了三个更fluent的方法。关于第一个,将保存方法端点调用结果的类定义解耦似乎有点荒谬。是的,有时您只想反序列化返回数据的特定键值,然后有时像这样,让fluent端点方法“知道”模型是什么更有意义数据去。这里就是这种情况。

public static WorkflowPacket GetStatesAndCounties(this WorkflowPacket wp, string containerName)
{var resp = RestService.Get<StateModel>($"{wp.BaseUrl}/States");wp.LastResponse = resp.status;wp.Container[containerName] = resp.item;return wp;
}public static WorkflowPacket AddState(this WorkflowPacket wp, string stateName)
{var resp = RestService.Post($"{wp.BaseUrl}/State", new { stateName });wp.LastResponse = resp.status;return wp;
}public static WorkflowPacket AddCounty
(this WorkflowPacket wp, string stateName, string countyName)
{var resp = RestService.Post($"{wp.BaseUrl}/State/${stateName}/County", new { countyName });wp.LastResponse = resp.status;return wp;
}

现在,我们需要在RestService中有一个Post方法:

public static (HttpStatusCode status, string content) Post(string url, object data = null)
{var response = Execute(Method.POST, url, data);return (response.StatusCode, response.Content);
}

当然,该Execute方法现在必须支持请求中的数据:

private static IRestResponse Execute(Method method, string url, object data = null)
{var client = new RestClient(url);var request = new RestRequest(method);data.IfNotNull(() => request.AddJsonBody(data));var response = client.Execute(request);return response;
}

集成测试

现在我们可以编写正面和负面的集成测试:

[TestMethod]
public void AddStateTest()
{string baseUrl = "http://localhost/FluentWebApiIntegrationtestDemo";new WorkflowPacket(baseUrl).AddState("NY").IShouldSeeNoContentResponse().AddState("CT").IShouldSeeNoContentResponse().GetStatesAndCounties("myStates").IShouldSeeOKResponse().ThenIShouldSee<StateModel>("myStates", m => m.GetStates().Count().Should().Be(2));
}[TestMethod]
public void AddDuplicateStateTest()
{string baseUrl = "<a href="http://localhost/FluentWebApiIntegrationtestDemo">http://localhost/FluentWebApiIntegrationtestDemo</a>";new WorkflowPacket(baseUrl).AddState("NY").IShouldSeeNoContentResponse().AddState("NY").IShouldSeeBadRequestResponse();
}[TestMethod]
public void AddCountyTest()
{string baseUrl = "<a href="http://localhost/FluentWebApiIntegrationtestDemo">http://localhost/FluentWebApiIntegrationtestDemo</a>";new WorkflowPacket(baseUrl).AddState("NY").IShouldSeeNoContentResponse().AddCounty("NY", "Columbia").GetStatesAndCounties("myStates").IShouldSeeOKResponse().ThenIShouldSee<StateModel>("myStates", m => m.GetStates().Count().Should().Be(1)).ThenIShouldSee<StateModel>("myStates", m => m.GetStates().First().Should().Be("NY")).ThenIShouldSee<StateModel>("myStates", m => m.GetCounties("NY").Count().Should().Be(1)).ThenIShouldSee<StateModel>("myStates", m => m.GetCounties("NY").First().Should().Be("Columbia"));
}[TestMethod]
public void AddCountyNoStateTest()
{string baseUrl = "http://localhost/FluentWebApiIntegrationtestDemo";new WorkflowPacket(baseUrl).AddCounty("NY", "Columbia").IShouldSeeBadRequestResponse();
}[TestMethod]
public void AddDuplicateCountyTest()
{string baseUrl = "http://localhost/FluentWebApiIntegrationtestDemo";new WorkflowPacket(baseUrl).AddState("NY").IShouldSeeNoContentResponse().AddCounty("NY", "Columbia").IShouldSeeNoContentResponse().AddCounty("NY", "Columbia").IShouldSeeBadRequestResponse();
}

运行测试

我们在最简单的集成测试中遇到了问题,即添加状态。

查看Postman中的响应,我们看到:

"The JSON value could not be converted to System.String. Path: $ | LineNumber: 0 | BytePositionInLine: 1."

哎呀。原因很明显——我将body参数实现为一个string,而不是一个类。这也似乎是合理的,然后改变不仅仅是端点加入state,也添加county到state:

public class StateCountyName
{public string StateName { get; set; }public string CountyName { get; set; }
}[HttpPost("")]
public object AddState([FromBody] StateCountyName name)
{object ret = Try<StateModelException>(NoContent(), () => stateModel.AddState(name.StateName));return ret;
}[HttpPost("County")]
public object AddCounty([FromBody] StateCountyName name)
{object ret = Try<StateModelException>(NoContent(), () => stateModel.AddCounty(name.StateName, name.CountyName));return ret;
}

是的,我们可以为null值和空字符串添加验证,但我不会用这些细节让您感到厌烦。

现在,当我运行AddStateTest,我得到这个讨厌的异常:

Newtonsoft.Json.JsonSerializationException: Cannot deserialize the current JSON array
(e.g. [1,2,3]) into type 'FluentWebApiIntegrationTestDemoModels.StateModel'
because the type requires a JSON object (e.g. {"name":"value"}) to deserialize correctly.

那是因为我在做一些愚蠢的事情。"get states" API函数返回states, 作为string的列表,我们期望响应是StateModel。所以让我们解决这个问题:

[HttpGet("")]
public object GetStates()
{return Ok(stateModel);
}

当然,这里真正的问题是我们不应该暴露StateModel的内部字典,而是将其映射到不同的集合。但这不是本文的重点。

所以现在我看到:

好极了!但...

因为:

Expected wp.LastResponse to be NoContent because Did not expected "State already exists.",
but found BadRequest.

哎呀。对于我们的测试,我们需要重置我们的伪数据库。从技术上讲,这应该在每个测试的清理中完成,这实际上是一个API调用:

[TestCleanup]
public void CleanupData()
{string baseUrl = "http://localhost/FluentWebApiIntegrationtestDemo";new WorkflowPacket(baseUrl).CleanupStateTestData().IShouldSeeOKResponse();
}

实现为:

[HttpPost("CleanupTestData")]
public object CleanupTestData()
{stateModel = new StateModel();return Ok();
}

然而,这实际上是错误的做法,尤其是在调试集成测试时——我们真正想要的是每个测试运行之前清理测试数据!

[TestInitialize]
public void CleanupData()
{new WorkflowPacket(baseUrl).CleanupStateTestData().IShouldSeeOKResponse();
}

最后我们成功了:

最后,我厌倦了这行代码:

string baseUrl = "http://localhost/FluentWebApiIntegrationtestDemo";

在每一次测试中。因此,测试类将派生自一个可以扩展以执行其他设置/拆卸功能的Setup类。

public class Setup
{public static string baseUrl = "http://localhost/FluentWebApiIntegrationtestDemo";
}

这个概念可以扩展到参数化URL,以便可以使用不同的服务器(本地、测试、QA),以便可以在测试/部署过程的每个步骤执行集成测试。我经常使用Setup基类来执行登录/身份验证以及在多个集成测试中使用的复杂数据设置(总是通过进行端点调用!)。

审查

在这里,我们做了一些更有趣的事情,因为集成测试需要不止一个步骤。要添加的county,state必须首先存在。这个基本测试:

new WorkflowPacket(baseUrl).AddState("NY").IShouldSeeNoContentResponse().AddCounty("NY", "Columbia").GetStatesAndCounties("myStates").IShouldSeeOKResponse().ThenIShouldSee<StateModel>("myStates", m => m.GetStates().Count().Should().Be(1)).ThenIShouldSee<StateModel>("myStates", m => m.GetStates().First().Should().Be("NY")).ThenIShouldSee<StateModel>("myStates", m => m.GetCounties("NY").Count().Should().Be(1)).ThenIShouldSee<StateModel>("myStates", m => m.GetCounties("NY").First().Should().Be("Columbia"));

可以扩展以测试多个州和每个县的多个州是否得到正确处理,更新和删除名称是否有效,等等。如果我们使用真正的数据库,API端点返回带有主键字段的记录是合理的,然后可以使用该字段添加县,而不是指定州名称。并且如果你对你的主键有一个一致的命名约定(比如“ID”,为什么人们坚持在主键名中包含表名是我无法理解的),你可以实现fluent API方法来按名称查找对象,所以你可以写:

.AddState("nyState", "NY")
.AddCounty("columbiaCounty", "nyState", "Columbia")

并且实现看起来像:

AddCounty(string countyBucketName, string stateBucketName, string countyName)
{int id = (wp.Container[stateBucketName] as IHasId).ID;var resp = RestService.Post<County>($"{wp.BaseUrl}/State/{id}/County", new { countyName });wp.LastResponse = resp.status;wp.LastContent = resp.content;wp.Container[countyBucketName] = resp.item;
}

希望这个概念是有道理的——这个想法是在进一步调用fluent API方法时使用API端点返回的对象,假设您已经使用一些智能对模型和端点进行了编码。

Fluent的缺点

fluent API(不是端点API!)的问题在于,如果发生异常,您并不真正知道自己在方法调用链中的位置。为了帮助改善这个问题,我们可以实现一个方法调用列表,这样当发生故障时,我们可以向开发人员显示方法链中发生故障的位置。例如,每个fluent API方法都可以记录自身:

public static WorkflowPacket Factorial<T>
(this WorkflowPacket wp, string containerName, int n) where T: new()
{wp.Log("Factorial");...

如果我们始终如一地这样做,那么我们可以随时显示日志:

public static WorkflowPacket PrintLog(this WorkflowPacket wp)
{wp.CallLog.ForEach(item => wp.Write(item));return wp;
}public static WorkflowPacket Write(this WorkflowPacket wp, string msg)
{System.Diagnostics.Debug.WriteLine(msg);return wp;
}

如果我添加PrintLog到集成测试的末尾,其添加了state和county,我们将看到:

然而,这还不够。我们真正想要的是让测试清理打印出日志,因此对于测试失败,我们可以看到它失败的地方。首先,我们重构测试装置本身以为每个测试实例化WorkflowPacket:

private WorkflowPacket wp;[TestInitialize]
public void InitializeTest()
{wp = new WorkflowPacket(baseUrl).CleanupStateTestData().IShouldSeeOKResponse();
}[TestCleanup]
public void CleanupTest()
{wp.PrintLog();
}

现在每个测试都使用wp而不是实例化自己的WorkflowPacket。因此,自动创建状态的测试如下所示:

[TestMethod]
public void AddCountyAndAutoCreateStateTest()
{wp.AddCounty("NY", "Columbia").IShouldSeeNoContentResponse().GetStatesAndCounties("myStates").IShouldSeeOKResponse().ThenIShouldSee<StateModel>("myStates", m => m.GetStates().Count().Should().Be(1)).ThenIShouldSee<StateModel>("myStates", m => m.GetStates().First().Should().Be("NY")).ThenIShouldSee<StateModel>("myStates", m => m.GetCounties("NY").Count().Should().Be(1)).ThenIShouldSee<StateModel>("myStates", m => m.GetCounties("NY").First().Should().Be("Columbia"));
}

当然,哪个失败了,我们可以看到测试失败的步骤:

我们看到它在调用AddCounty。

结论——什么是模式?

这种方法的模式非常简单,你几乎可以从任何地方开始你喜欢做事的方式,当然这总是取决于实际要做的任务是什么!

一旦你养成了编写这类集成测试的习惯,它就变成了第二天性。我发现我实际上在接触任何代码之前编写集成测试:

  • 先证明代码有误;
  • 其次证明修复有效;
  • 第三,证明修复没有破坏其他东西。

我发现这种方法也比使用直接在浏览器上模拟用户操作的实际网页测试应用程序更有效。通过这种方法,我可以在实现UI之前编写Web API,并证明Web API根据规范工作。同样,如果我的Web API集成测试通过,则问题出在前端。

现在您可能会说,所有这些都可以通过单元测试来处理。我说不,它不能。在实际实践中,我处理复杂的相互依赖的数据,代码库没有设计为可单元测试的(从来都不是),并且业务规则分散在各种类实例和触发事件中。离散地测试其中任何一个都不会建立任何信心,即当用户单击“保存”按钮时,所有逻辑都会执行它应该做的事情。相反,使用集成测试,我可以通过其他端点(同时测试代码的其他部分)设置数据的所有不同配置,然后调用“Save"触发所有业务规则的API端点。从那里,我可以像用户看到的那样请求返回数据并验证一切看起来是否正确。

归根结底,以流畅的方式编写集成测试并使用FluentAssertions得到的只是简单的反馈:“哇,这实际上是可读的!” 希望你也会有这样的经历。我也希望您喜欢阅读我如何创建流畅的Web API集成测试“框架”以及其中的步骤和思考。

https://www.codeproject.com/Articles/5303342/Fluent-Web-API-Integration-Testing

Fluent Web API集成测试相关推荐

  1. ASP.NET Core Web API 集成测试中使用 Bearer Token

    在 ASP.NET Core Web API 集成测试一文中, 我介绍了ASP.NET Core Web API的集成测试. 在那里我使用了测试专用的Startup类, 里面的配置和开发时有一些区别, ...

  2. ASP.NET Core Web API 索引 (更新Identity Server 4 视频教程)

    GraphQL 使用ASP.NET Core开发GraphQL服务器 -- 预备知识(上) 使用ASP.NET Core开发GraphQL服务器 -- 预备知识(下) [视频] 使用ASP.NET C ...

  3. 如何测试ASP.NET Core Web API

    在本文中,我们将研究如何测试你的ASP .NET Core 2.0 Web API解决方案.我们将了解使用单元测试进行内部测试,使用全新的ASP .NET Core的集成测试框架来进行外部测试. 本文 ...

  4. 从头编写 asp.net core 2.0 web api 基础框架 (4) EF配置

    第一部分: https://www.cnblogs.com/frank0812/p/11165940.html 第二部分:https://www.cnblogs.com/frank0812/p/111 ...

  5. 如何测试 ASP.NET Core Web API

    在本文中,我们将研究如何测试你的 ASP .NET Core 2.0 Web API 解决方案.我们将了解使用单元测试进行内部测试,使用全新的 ASP .NET Core 的集成测试框架来进行外部测试 ...

  6. ASP.NET Core 3.1 Web API和EF Core 5.0 中具有泛型存储库和UoW模式的域驱动设计实现方法

    目录 介绍 背景 领域驱动设计 存储库模式 工作单元模式 使用代码 创建空白解决方案和解决方案架构 添加和实现应用程序共享内核库 PageParam.cs 在Entity Framework Core ...

  7. 使用ASP.NET Web API和Handlebars的Web模板

    目录 介绍 目标听众 期待什么 示例代码概述 总览 Handlebars和模板 使用代码 起步 第1步 从GitHub下载 介绍 Web应用程序的开发趋势不时发生变化.几年前我们用来构建的应用程序体系 ...

  8. 生成用于ASP.NET Web API的C#客户端API

    目录 介绍 主要特征 主要好处 背景 推定(Presumptions) 使用代码 步骤0:将NuGet软件包WebApiClientGen安装到Web MVC/API项目 步骤1:建立.NET Cli ...

  9. 在ASP.NET Core 2.0中创建Web API

    目录 介绍 先决条件 软件 技能 使用代码 第01步 - 创建项目 第02步 - 安装Nuget包 步骤03 - 添加模型 步骤04 - 添加控制器 步骤05 - 设置依赖注入 步骤06 - 运行We ...

最新文章

  1. 【Servlet】Cookie应用:显示上次访问页面时间
  2. 抽象类,接口都与继承有关
  3. 嵌入式基础之----C语言
  4. python取前三位_python3 获取前几个高频列表元素
  5. “约见”面试官系列之常见面试题之第八十六篇之nexttick(建议收藏)
  6. 自学 Python 到什么程度能找到工作,1300+ 条招聘信息告诉你答案
  7. python 坐标轴刻度 格式_matplotlib命令与格式之tick坐标轴日期格式(设置日期主副刻度)...
  8. UnixLinux大学教程目录
  9. 基于Solana区块链的去中心化交易所Orca正式启动
  10. 中国双面泡棉胶带市场趋势报告、技术动态创新及市场预测
  11. java抓取网页数据_简易数据分析 10 | Web Scraper 翻页——抓取滚动加载类型网页...
  12. Python 格式化字符串f-string概览(转载)
  13. excel/vosviewer词频统计的方法
  14. Tomcat启动页面中文乱码解决方法
  15. 输出数值类型的算法评价指标
  16. 基于AM5728核心板的户外工作站可靠性和便捷性设计
  17. 开源项目ruoyi-springboot-vue源码分析之LogAspect日志打印
  18. revit怎么上色?教你revit综合工具快速【元素上色】
  19. 03_JavaScript常见运算符
  20. Stimulsoft Dashboards.WEB 23.1.8 完美Patch

热门文章

  1. mysql+导入+306_mysql常用命令二
  2. 电商产品页多种出彩表现设计手法!
  3. UI设计灵感|3D\C4D元素网站,流行最前沿
  4. 古典绘画水墨文化艺术插图手绘合集,再也不愁没有设计灵感!
  5. 尽显中国风 | 高品质海报背景,PSD分层,智能替换展示商品
  6. UI设计素材模板|游戏APP界面
  7. vps没有mysql怎么用商店_如何在本地搞一个小程序的服务器之我没有vps我也很绝望呀...
  8. CUDA 开启GPU之间的P2P通信功能
  9. Linux内核:了解Linux内核抢占
  10. Pango Reference Manual 【文本和字体处理函数库】