Наверное каждый, кто занимался UI тестированием приложений, слышал о Page Object и Page Factory патернах проектирования. И, уж точно, каждый из них знает в чем их преимущество. Но когда дело доходит до API, то тут начинается импровизация. Кто как придумал, кто как смог… кто-то формирует запросы в файлах, кто-то в классас, а кто-то и в тест методах не брезгует. Ниже я опишу вам как решал этот вопрос я у себя на проекте.
А давайте делать как в Page Object pattern
Ну правда, давайте разделим наши запросы на Resources и Actions по аналогии с Pages и Steps. В Resouces будут находиться статические данные к endpoint’ам, а в Actions мы добавим динамические данные и выполним сам запрос. Статические данные будем хранить в атрибутах Request и Format. Request содержит метод запроса и url к endpoint, а Format определяет формат данных которые будут отправлены или получены. Опишем условный Users ресурс нашего приложения со стандартным набором CRUD операций.
internal class UserResource
{
[Request(Method.GET, "users")]
public Endpoint GetAll { get; set; }
[Request(Method.GET, "users/{userId}")]
public Endpoint Get { get; set; }
[Request(Method.POST, "users")]
[Format(DataFormat.Json)]
public Endpoint Add { get; set; }
[Request(Method.PUT, "users/{userId}")]
[Format(DataFormat.Json)]
public Endpoint Update { get; set; }
[Request(Method.DELETE, "users/{userId}")]
public Endpoint Delete { get; set; }
}
А теперь посмотрим как выглядит Actions для UsersResource
public class UsersActions
{
private UsersResource UsersResource => Resource.Get<UsersResource>();
public IEnumerable<User> GetUsers()
{
return UsersResource
.GetAll
.Execute<IEnumerable<User>>();
}
public User GetUserById(long userId)
{
return UsersResource
.Get
.WithUrlSegment("userId", userId)
.Execute<User>();
}
public void AddUser(User user)
{
UsersResource
.Add
.WithData(user)
.Execute();
}
public void UpdateUser(long userId, User user)
{
UsersResource
.Update
.WithUrlSegment("userId", userId)
.WithData(user)
.Execute();
}
public void DeleteUser(long userId)
{
UsersResource
.Delete
.WithUrlSegment("userId", userId)
.Execute();
}
}
Выглядит просто и лаконично, правда? Но пока не понятно, что за тип такой Endpoint. Endpoint - это класс билдер зпроса. У себя на проекте я использую RestSharp, так что Endpoint - это обертка над RestRequest, но всегда можно использовать что-то другое.
public class Endpoint
{
private readonly RestRequest _request = new RestRequest();
private readonly RestClient _restClient;
public Endpoint(RestClient client)
{
_restClient = client;
}
public Endpoint WithMethod(Method method)
{
_request.Method = method.Convert();
return this;
}
public Endpoint WithResource(string resource)
{
_request.Resource = resource;
return this;
}
public Endpoint WithDataFormat(DataFormat format)
{
_request.RequestFormat = format.Convert();
return this;
}
public Endpoint WithUrlSegment(string name, object value)
{
_request.AddUrlSegment(name, value.ToString());
return this;
}
public Endpoint WithParameter(string name, object value)
{
_request.AddQueryParameter(name, value.ToString());
return this;
}
public Endpoint WithData(object body)
{
_request.AddBody(body);
return this;
}
public void Execute()
{
_restClient.Execute(_request);
}
internal T Execute<T>()
{
var content = _restClient.Execute(_request).Content;
return JsonConvert.DeserializeObject<T>(content);
}
}
Такой подход позволяет построить простоую в поддержке и использовании структуру проекта. Все изменения происходят в одном месте и нет повторного дублироввания кода. Сам проект можно посмотреть и скачать на моем профиле GitHub.
Comments