使用 Specflow 测试 Web API - 第一部分






4.64/5 (8投票s)
使用 SpecFlow 测试 Web API
引言
使用 Specflow 进行自动化测试是为受保护/未受保护的服务提供测试覆盖率的有效方法。本文将展示如何编写针对未受保护的 WebAPI 服务的测试场景。
必备组件
您需要具备 Web API 的基本知识。
您可以按照这个链接了解有关设置 Specflow 的初始介绍。
解决方案
PropertiesAPI
是一个WebAPI
服务,它公开 CRUD 端点以管理属性PropertiesAPI.CashedRepository
使用ObjectCache
进行 CRUD 操作PropertiesAPI.AcceptanceTests
用于验收测试。 它包括针对 Web API 主要端点的场景。PropertiesAPI.Contracts
用于在WebAPI
、CachedRepository
之间共享实体
Using the Code
第一部分:Web API
POST 请求
它将创建一个新属性
[HttpPost]
public IHttpActionResult Post(PropertyModel property)
{
if (!ModelState.IsValid)
return BadRequest();
_propertiesPresenter.CreateProperty(property);
return ResponseMessage(new HttpResponseMessage
{
ReasonPhrase = "Property has been created",
StatusCode = HttpStatusCode.Created
});
}
PUT 请求
它将更新一个属性
[HttpPut]
public IHttpActionResult Put(PropertyModel property)
{
_propertiesPresenter.UpdateProperty(property);
return
}
DELETE 请求
它将根据 Id
删除一个属性
[HttpDelete]
public IHttpActionResult Delete(int id)
{
_propertiesPresenter.DeleteProperty(id);
return Ok();
}
带有分页的 GET 请求
[EnableETag]
[HttpGet]
public IHttpActionResult Get([FromUri]PropertySearchModel model)
{
var properties = _propertiesPresenter.GetProperties(model);
var totalCount = properties.Count; ;
var totalPages = (int)Math.Ceiling((double)totalCount /model.PageSize);
var urlHelper = new UrlHelper(Request);
var prevLink = model.Page > 0 ? Url.Link("DefaultApi",
new { controller = "Properties", page = model.Page - 1 }) : "";
var nextLink = model.Page < totalPages - 1 ? Url.Link("DefaultApi",
new { controller = "Properties", page = model.Page + 1 }) : "";
var paginationHeader = new
{
TotalCount = totalCount,
TotalPages = totalPages,
PrePageLink = prevLink,
NextPageLink = nextLink
};
HttpContext.Current.Response.Headers.Add("X-Pagination",
JsonConvert.SerializeObject(paginationHeader));
var results = properties
.Skip(model.PageSize * model.Page)
.Take(model.PageSize)
.ToList();
//Results
return Ok(results);
}
根据此端点,我们请求带有分页的结果。 分页应作为响应头“X-Pagination
”返回。
第二部分:Feature 文件
Properties.Feature 文件列出了所有场景。 功能文件中的每个场景都会测试 PropertiesAPI
中的一个端点
添加属性 针对 PropertiesAPI
的 POST
请求
更新属性 场景将针对 PUT
请求以更新属性
步骤的顺序假定在我们更新 Property
之前,第一步是创建一个。 第一步Given I create a new property 可以被认为是其他场景重用的通用步骤。
删除属性 场景将针对 DELETE
请求以删除属性
EnableEtag 属性
其中一个端点已使用 EnableEtag
属性进行修饰。 ETag
是服务器为特定资源生成的唯一键 (string
)。 下次客户端请求相同的资源时,服务器将返回 304,并将密钥添加到响应的标头中。 简单来说,服务器告诉客户端“您可以使用已有的内容,因为它尚未更新”。
我们要测试以下流程是否有效
- 服务器将生成一个密钥并将其添加到响应中。 但是,由于这是我们第一次使用此端点,因此它将响应 200。
- 下次,如果客户端请求相同的资源,如果响应没有更新并且缓存的到期时间在我们定义的限制内(当前的
ETag
实现正在使用 Web 缓存),那么服务器将响应 304。
GET
请求以检索带有分页的属性第三部分:步骤
添加属性 场景的步骤
[Given(@"I create a new property \((.*),(.*),(.*),(.*),(.*)\)")]
public void GivenICreateANewProperty
(string Address, decimal Price, string Name, string PropertyDescription, int Id)
{
_property = new Property()
{
Address = Address,
Name = Name,
Price = Price,
PropertyDescription = PropertyDescription,
Id = Id
};
var request = new HttpRequestWrapper()
.SetMethod(Method.POST)
.SetResourse("/api/properties/")
.AddJsonContent(_property);
_restResponse = new RestResponse();
_restResponse = request.Execute();
_statusCode = _restResponse.StatusCode;
ScenarioContext.Current.Add("Pro", _property);
}
[Given(@"ModelState is correct")]
public void GivenModelStateIsCorrect()
{
Assert.That(() => !string.IsNullOrEmpty(_property.Address));
Assert.That(() => !string.IsNullOrEmpty(_property.Name));
Assert.That(() => !string.IsNullOrEmpty(_property.PropertyDescription));
Assert.That(() => _property.Price.HasValue);
}
[Then(@"the system should return properties that match my criteria")]
public void ThenTheSystemShouldReturn()
{
Assert.AreEqual(_statusCode, HttpStatusCode.Created);
}
ScenarioContext.Current
ScenarioContext.Current
是框架的缓存机制,用于保存我们需要在测试之间共享的数据。 请注意,在每个场景的开始,此缓存都会被清除。
更新属性 场景的步骤
[When(@"I update an existing property \((.*),(.*)\)")]
public void WhenIUpdateAnExistingProperty(string newAddress, decimal newPrice)
{
_property.Address = newAddress;
_property.Price = newPrice;
var request = new HttpRequestWrapper()
.SetMethod(Method.PUT)
.SetResourse("/api/properties/")
.AddJsonContent(_property);
//_restResponse = new RestResponse();
var response = request.Execute();
}
[Given(@"I request to view properties with pagination \((.*),(.*),(.*),(.*),(.*),(.*)\)")]
[When(@"I request to view properties with pagination \((.*),(.*),(.*),(.*),(.*),(.*)\)")]
[Then(@"I request to view properties with pagination \((.*),(.*),(.*),(.*),(.*),(.*)\)")]
public void GivenIRequestToViewPropertiesWithPagination
(int page, int pageSize, string address, decimal priceMin, decimal priceMax, int Id)
{
_property = ScenarioContext.Current.Get<Property>("Pro");
var request = new HttpRequestWrapper()
.SetMethod(Method.GET)
.SetResourse("/api/properties/")
.AddParameters(new Dictionary<string, object>() {
{ "Page", page },
{ "PageSize", pageSize },
{ "PriceMin", priceMin },
{ "PriceMax", priceMax },
{ "Address", address },
{ "Id", _property.Id },
});
_restResponse = new RestResponse();
_restResponse = request.Execute();
_statusCode = _restResponse.StatusCode;
_properties = JsonConvert.DeserializeObject<List<Property>>(_restResponse.Content);
}
[Then(@"the updated property should be included in the list")]
public void ThenTheUpdatedPropertyShouldBeIncludedInTheList()
{
Assert.That(() => _properties.Contains(_property));
}
更新属性 场景中的第一步与添加属性和删除属性 场景中的第一步相同。 我们不应该为其他场景重写相同的步骤,而是可以重用它们作为通用步骤。 我们应该尝试创建可重用的步骤,以便尽可能多地服务于场景。 我们拥有的可重用步骤越多,设计旨在测试代码中各种情况的场景就越容易和快速。
另一个可重用的步骤是 GivenIRequestToViewPropertiesWithPagination
。 但是,此步骤不仅由 WHEN
步骤调用,而且还由 Given
和 Then
步骤调用。 我们需要通过使用以下属性修饰它来通知框架这是一个通用步骤
[Given(@"I request to view properties with pagination \((.*),(.*),(.*),(.*),(.*),(.*)\)")]
[When(@"I request to view properties with pagination \((.*),(.*),(.*),(.*),(.*),(.*)\)")]
[Then(@"I request to view properties with pagination \((.*),(.*),(.*),(.*),(.*),(.*)\)") ]
第四部分:创建请求的类
我们需要在验收测试项目中安装 RestSharp。 HttpRequestWrapper
封装了我们感兴趣的 RestSharp 功能,用于发出请求。
Install-Package RestSharp
public class HttpRequestWrapper
{
private RestRequest _restRequest;
private RestClient _restClient;
public HttpRequestWrapper()
{
_restRequest = new RestRequest();
}
public HttpRequestWrapper SetResourse(string resource)
{
_restRequest.Resource = resource;
return this;
}
public HttpRequestWrapper SetMethod(Method method)
{
_restRequest.Method = method;
return this;
}
public HttpRequestWrapper AddHeaders(IDictionary<string,string> headers)
{
foreach (var header in headers)
{
_restRequest.AddParameter(header.Key, header.Value, ParameterType.HttpHeader);
}
return this;
}
public HttpRequestWrapper AddJsonContent(object data)
{
_restRequest.RequestFormat = DataFormat.Json;
_restRequest.AddHeader("Content-Type", "application/json");
_restRequest.AddBody(data);
return this;
}
public HttpRequestWrapper AddEtagHeader(string value)
{
_restRequest.AddHeader("If-None-Match", value);
return this;
}
public HttpRequestWrapper AddParameter(string name, object value)
{
_restRequest.AddParameter(name, value);
return this;
}
public HttpRequestWrapper AddParameters(IDictionary<string,object> parameters)
{
foreach (var item in parameters)
{
_restRequest.AddParameter(item.Key, item.Value);
}
return this;
}
public IRestResponse Execute()
{
try
{
_restClient = new RestClient("https://:50983/");
var response = _restClient.Execute(_restRequest);
return response;
}
catch (Exception ex)
{
throw;
}
}
public T Execute<T>()
{
_restClient = new RestClient("https://:50983/");
var response = _restClient.Execute(_restRequest);
var data = JsonConvert.DeserializeObject<T>(response.Content);
return data;
}
}
未完待续..
下一部分将重点介绍如何为需要身份验证和授权的 API 编写测试。