65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.64/5 (8投票s)

2016年3月18日

CPOL

3分钟阅读

viewsIcon

111404

downloadIcon

1469

使用 SpecFlow 测试 Web API

引言

使用 Specflow 进行自动化测试是为受保护/未受保护的服务提供测试覆盖率的有效方法。本文将展示如何编写针对未受保护的 WebAPI 服务的测试场景。

必备组件

您需要具备 Web API 的基本知识。

您可以按照这个链接了解有关设置 Specflow 的初始介绍。

解决方案

  • PropertiesAPI 是一个 WebAPI 服务,它公开 CRUD 端点以管理属性
  • PropertiesAPI.CashedRepository 使用 ObjectCache 进行 CRUD 操作
  • PropertiesAPI.AcceptanceTests 用于验收测试。 它包括针对 Web API 主要端点的场景。
  • PropertiesAPI.Contracts 用于在 WebAPICachedRepository 之间共享实体

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 中的一个端点

添加属性 针对 PropertiesAPIPOST 请求

更新属性 场景将针对 PUT 请求以更新属性

步骤的顺序假定在我们更新 Property 之前,第一步是创建一个。 第一步Given I create a new property 可以被认为是其他场景重用的通用步骤

删除属性 场景将针对 DELETE 请求以删除属性

EnableEtag 属性

其中一个端点已使用 EnableEtag 属性进行修饰。 ETag 是服务器为特定资源生成的唯一键 (string)。 下次客户端请求相同的资源时,服务器将返回 304,并将密钥添加到响应的标头中。 简单来说,服务器告诉客户端“您可以使用已有的内容,因为它尚未更新”。

我们要测试以下流程是否有效

  1. 服务器将生成一个密钥并将其添加到响应中。 但是,由于这是我们第一次使用此端点,因此它将响应 200。
  2. 下次,如果客户端请求相同的资源,如果响应没有更新并且缓存的到期时间在我们定义的限制内(当前的 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 步骤调用,而且还由 GivenThen 步骤调用。 我们需要通过使用以下属性修饰它来通知框架这是一个通用步骤

[Given(@"I request to view properties with pagination \((.*),(.*),(.*),(.*),(.*),(.*)\)")]
[When(@"I request to view properties with pagination \((.*),(.*),(.*),(.*),(.*),(.*)\)")]
[Then(@"I request to view properties with pagination \((.*),(.*),(.*),(.*),(.*),(.*)\)") ]

第四部分:创建请求的类

我们需要在验收测试项目中安装 RestSharpHttpRequestWrapper 封装了我们感兴趣的 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 编写测试。

Resource

© . All rights reserved.