Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Using Specflow to Test Web API - PART 1

4.64/5 (8 votes)
7 Mar 2017CPOL3 min read 110K   1.4K  
Using SpecFlow to Test Web API

Introduction

Automated testing using Specflow is an efficient way to provide test coverage to secured/non-secured services. The article will show how to write testing Scenarios that target a nonsecured WebAPI service.

Prerequisites

You need to have basic knowledge of Web API.

You can see an initial introduction regarding setting up Specflow following this link.

The Solution

  • PropertiesAPI is a WebAPI service that exposes CRUD endpoints to manage Properties
  • PropertiesAPI.CashedRepository that uses ObjectCache for CRUD operations
  • PropertiesAPI.AcceptanceTests for acceptance tests. It includes scenarios that target the main endpoints of the Web API.
  • PropertiesAPI.Contracts for sharing entities between WebAPI, CachedRepository

Using the Code

Part I: The Web API

POST REQUEST

It will create a new property:

C#
[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 REQUEST

It will update a property:

C#
[HttpPut]
public IHttpActionResult Put(PropertyModel property)
{
    _propertiesPresenter.UpdateProperty(property);
    return
}

DELETE REQUEST

It will delete a property based on Id:

C#
[HttpDelete]
public IHttpActionResult Delete(int id)
{
    _propertiesPresenter.DeleteProperty(id);
    return Ok();
}

GET REQUEST WITH PAGINATION

C#
[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);
 }

According to this endpoint, we request results with pagination. The pagination should be returned as a response header "X-Pagination".

PART II: Feature File

The Properties.Feature file lists all the Scenarios. Each Scenario, in the feature file, tests an endpoint in the PropertiesAPI:

The Add Property that targets the POST request of the PropertiesAPI:

The Update Properties scenario will target the PUT request for updating properties:

The order of the steps assumes that before we update a Property, the first step is to create one. The first step Given I create a new property can be considered as a common step to be reused by the other scenarios.

The Delete Properties scenario will target the DELETE request for deleting a property:

EnableEtag Attribute

One of the endpoints has been decorated with EnableEtag attribute. ETag is a unique key (string) generated at the server for a particular resource. The next time the client requests the same resource, the server will return 304 and with the key added to the Header of the response. To put it in simple words, the server tells the Client "You can use what you already have since it has not been updated".

We want to test whether the following flow works:

  1. The server will generate a Key and add it to the response. However, as it is the first time we are using this endpoint, it will respond with 200.
  2. Next time, the client requests the same resource, if the response has not been updated and the time expiry of the cache is within the limits we defined (current ETag implementation is using web caching), then the server will respond with 304.

The Get Properties Scenario will target the GET request for retrieving properties with pagination:

Part III: The Steps

Steps for Add Property Scenario:

C#
[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 is the framework's caching mechanism, in order to save data that we need to share between tests. Note that in the beginning of each scenario, this cache is cleared.

Steps for Update Properties Scenario:

C#
[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));
 }

The first step in the Update Properties Scenario is the same as the first step in the Add Property and Delete Property Scenario. We shouldn't rewrite the same step for other scenarios, but instead we can reuse them as common steps. We should try to create reusable steps that can serve as many scenarios as possible. The more reusable steps we have, the easier and faster it will be, to design scenarios that aim to test various cases in our code.

One more reusable step is GivenIRequestToViewPropertiesWithPagination. However, this step is not called only by WHEN Steps , but also by Given and Then steps. We need to notify the framework that this is a common step, by decorating it with the following attributes:

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

PART IV: Class to Create Requests

We need to install RestSharp in the Acceptance Tests projects. HttpRequestWrapper encapsulates the RestSharp functionality that we are interested in, for making requests.

Install-Package RestSharp 
C#
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("http://localhost:50983/");
               var response = _restClient.Execute(_restRequest);
               return response;
           }
           catch (Exception ex)
           {
               throw;
           }
       }

       public T Execute<T>()
       {
           _restClient = new RestClient("http://localhost:50983/");
           var response = _restClient.Execute(_restRequest);
           var data = JsonConvert.DeserializeObject<T>(response.Content);
           return data;
       }

 }

To Be Continued ..

The next part will focus on how we can write tests for APIs that require Authentication and Authorization.

Resource

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)