Click here to Skip to main content
15,860,972 members
Articles / Web Development / HTML

Using Specflow to Test Web API - PART 1

Rate me:
Please Sign up or sign in to vote.
4.64/5 (8 votes)
7 Mar 2017CPOL3 min read 104.3K   1.4K   13   25
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)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionApi automation using restsharp and specflow BDD Pin
Member 1596582729-Mar-23 21:43
Member 1596582729-Mar-23 21:43 
QuestionAPI Automation Framework Resharp + Specflow Pin
Member 150101292-Dec-20 8:43
Member 150101292-Dec-20 8:43 
AnswerRe: API Automation Framework Resharp + Specflow Pin
Veronica S. Zotali25-Jan-21 12:24
Veronica S. Zotali25-Jan-21 12:24 
QuestionSpecflow Part 2 Request Pin
Member 146171439-Oct-19 1:20
Member 146171439-Oct-19 1:20 
QuestionComplete code on API Automation(RestSharp, C#, Specflow) Pin
Dinesh Kumar Solanki20-Nov-18 9:59
Dinesh Kumar Solanki20-Nov-18 9:59 
QuestionRest Shap Automation Pin
Member 1398204112-Sep-18 4:14
Member 1398204112-Sep-18 4:14 
QuestionUsing Specflow to test Web API - PART 2 Pin
Member 138728402-Aug-18 6:31
Member 138728402-Aug-18 6:31 
QuestionPart2 Pin
Member 138625206-Jun-18 22:30
Member 138625206-Jun-18 22:30 
QuestionAPI testing Pin
suraj chirde4-Apr-18 3:51
suraj chirde4-Apr-18 3:51 
QuestionRequired Using Specflow to Test Web API - PART 2 Pin
Member 1373060316-Mar-18 7:36
Member 1373060316-Mar-18 7:36 
QuestionUsing Specflow to Test Web API - PART 1 - code request Pin
Francisco Nieves2-May-17 5:53
Francisco Nieves2-May-17 5:53 
AnswerRe: Using Specflow to Test Web API - PART 1 - code request Pin
Veronica S. Zotali6-May-17 10:42
Veronica S. Zotali6-May-17 10:42 
QuestionAdvantages of SpecFlow with APIs Pin
Wrexxi8-Mar-17 7:16
professionalWrexxi8-Mar-17 7:16 
QuestionUsing Specflow to test Web API - PART 2 Pin
Member 1259375520-Jun-16 3:12
Member 1259375520-Jun-16 3:12 
AnswerRe: Using Specflow to test Web API - PART 2 Pin
Veronica S. Zotali21-Jun-16 14:14
Veronica S. Zotali21-Jun-16 14:14 
PraiseRe: Using Specflow to test Web API - PART 2 Pin
pentanareshkumar12-Mar-17 16:56
pentanareshkumar12-Mar-17 16:56 
GeneralRe: Using Specflow to test Web API - PART 2 Pin
Veronica S. Zotali13-Mar-17 3:03
Veronica S. Zotali13-Mar-17 3:03 
GeneralRe: Using Specflow to test Web API - PART 2 Pin
Nickelright18-Sep-18 23:04
Nickelright18-Sep-18 23:04 
GeneralRe: Using Specflow to test Web API - PART 2 Pin
Member 142035142-Apr-19 9:19
Member 142035142-Apr-19 9:19 
QuestionRunning the tests Pin
mick_lennon21-Mar-16 1:23
mick_lennon21-Mar-16 1:23 
AnswerRe: Running the tests Pin
Veronica S. Zotali21-Mar-16 1:40
Veronica S. Zotali21-Mar-16 1:40 
GeneralRe: Running the tests Pin
mick_lennon21-Mar-16 1:57
mick_lennon21-Mar-16 1:57 
GeneralRe: Running the tests Pin
Veronica S. Zotali21-Mar-16 3:11
Veronica S. Zotali21-Mar-16 3:11 
GeneralRe: Running the tests Pin
mick_lennon21-Mar-16 4:28
mick_lennon21-Mar-16 4:28 
GeneralRe: Running the tests Pin
Veronica S. Zotali21-Mar-16 5:06
Veronica S. Zotali21-Mar-16 5:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.