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:
[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:
[HttpPut]
public IHttpActionResult Put(PropertyModel property)
{
_propertiesPresenter.UpdateProperty(property);
return
}
DELETE REQUEST
It will delete a property based on Id
:
[HttpDelete]
public IHttpActionResult Delete(int id)
{
_propertiesPresenter.DeleteProperty(id);
return Ok();
}
GET REQUEST WITH PAGINATION
[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();
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:
- 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.
- 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:
[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:
[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);
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:
[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
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