REST API тестирование. Организация проекта

- 7 mins

Наверное каждый, кто занимался 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 : Resource
{
    [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);
    }
}

Если в голове возник вопрос: “А где же Page Factory для наших Page Objects?”, то овет ниже. Это не PageFactory, это Resource который инициализирует все поля Resource слассов.

public class Resource
{
    private static readonly ThreadLocal<RestClient> Client = new ThreadLocal<RestClient>();

    public static T Get<T>() where T : Resource, new()
    {
        var resource = new T();
        foreach (PropertyInfo endpointProperty in GetEndpointProperties(typeof(T)))
        {
            if (!Client.IsValueCreated)
                Client.Value = new RestClient();
            var endpoint = new Endpoint(Client.Value);
            SetRequest(endpoint, endpointProperty);
            SetFormat(endpoint, endpointProperty);
            endpointProperty.SetValue(resource, endpoint);
        }
        return resource;
    }

    private static void SetFormat(Endpoint endpoint, PropertyInfo propertyInfo)
    {
        var format = propertyInfo.GetCustomAttribute<FormatAttribute>();
        if (format == null) return;
        endpoint.WithDataFormat(format.DataFormat);
    }

    private static void SetRequest(Endpoint endpoint, PropertyInfo propertyInfo)
    {
        var request = propertyInfo.GetCustomAttribute<RequestAttribute>();
        if (request == null) return;
        endpoint.WithMethod(request.Method);
        endpoint.WithResource(request.Resource);
    }

    private static IEnumerable<PropertyInfo> GetEndpointProperties(Type type)
    {
        return type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
            .Where(i => i.PropertyType == typeof(Endpoint));
    }
}

Такой подход позволяет построить простоую в поддержке и использовании структуру проекта. Все изменения происходят в одном месте и нет повторного дублироввания кода. Сам проект можно посмотреть и скачать на моем профиле GitHub.

Yurii Hunter

Yurii Hunter

A Man who develops software with coffee

comments powered by Disqus
rss facebook twitter github telegram gitlab youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora quora