Как написать функциональные тесты для API ServiceStack

У нас есть веб-приложение ASP.NET, подключенное к ServiceStack. Раньше я никогда не писал функциональных тестов, но мне было поручено написать тесты (nUnit) против нашего API и доказать, что он работает до уровня базы данных.

Может кто-нибудь помочь мне начать писать эти тесты?

Вот пример метода post в службе наших пользователей.

public object Post( UserRequest request )
{
    var response = new UserResponse { User = _userService.Save( request ) };

    return new HttpResult( response )
    {
        StatusCode = HttpStatusCode.Created,
        Headers = { { HttpHeaders.Location, base.Request.AbsoluteUri.CombineWith( response.User.Id.ToString () ) } }
    };
}

Теперь я знаю, как написать стандартный Unit Test, но я смущен в этой части. Должен ли я вызывать WebAPI через HTTP и инициализировать post? Я просто вызываю метод, как я бы unit test? Я полагаю, что часть "Functional Test" ускользает от меня.

Ответ 1

Тестирование договора на обслуживание

Для сквозного функционального теста я сосредоточен на проверке того, что служба может принимать сообщение запроса и выводить ожидаемое ответное сообщение для простых случаев использования.

Веб-служба - это контракт: с учетом сообщения определенной формы служба будет выдавать ответное сообщение данной формы. И secondarialy, услуга определенным образом изменит состояние своей базовой системы. Обратите внимание, что для конечного клиента сообщение не является вашим классом DTO, а конкретным примером запроса в данном текстовом формате (JSON, XML и т.д.), Отправленным с определенным глаголом на определенный URL-адрес, с заданным набором заголовков.

Существует несколько уровней веб-службы ServiceStack:

client -> message -> web server -> ServiceStack host -> service class -> business logic

Простое тестирование модулей и интеграция лучше всего подходит для уровня бизнес-логики. Как правило, они легко обрабатывают единичные тесты непосредственно с вашими классами обслуживания: легко создать объект DTO, вызвать метод Get/Post в вашем классе обслуживания и проверить объект ответа. Но они не тестируют ничего, что происходит внутри хоста ServiceStack: маршрутизация, сериализация/десериализация, выполнение фильтров запросов и т.д. Конечно, вы не хотите тестировать сам код ServiceStack как тот код инфраструктуры, который имеет свои собственные модульные тесты, Но есть возможность проверить конкретный путь, по которому сообщение конкретного запроса переходит в службу и выходит из нее. Это часть контракта на обслуживание, которая не может быть полностью проверена, глядя прямо на класс обслуживания.

Не пытайтесь покрыть 100%

Я бы не рекомендовал, чтобы эти функциональные тесты получили 100% -ное покрытие всей бизнес-логики. Я сосредотачиваюсь на освещении основных вариантов использования этих тестов - обычно один или два примера запроса на конечную точку. Подробное тестирование конкретных случаев бизнес-логики гораздо более эффективно выполняется путем написания традиционных модульных тестов против ваших бизнес-логических классов. (Ваша бизнес-логика и доступ к данным не реализованы в ваших сервисных классах ServiceStack, верно?)

Реализация

Мы собираемся запустить службу ServiceStack в процессе и использовать HTTP-клиент для отправки запросов на него, а затем проверить содержимое ответов. Эта реализация специфична для NUnit; аналогичная реализация должна быть возможной в других рамках.

Во-первых, вам нужно установить устройство настройки NUnit, которое запускает один перед всеми вашими тестами, чтобы настроить хост-узел ServiceStack:

// this needs to be in the root namespace of your functional tests
public class ServiceStackTestHostContext
{
    [TestFixtureSetUp] // this method will run once before all other unit tests
    public void OnTestFixtureSetUp()
    {
        AppHost = new ServiceTestAppHost();
        AppHost.Init();
        AppHost.Start(ServiceTestAppHost.BaseUrl);
        // do any other setup. I have some code here to initialize a database context, etc.
    }

    [TestFixtureTearDown] // runs once after all other unit tests
    public void OnTestFixtureTearDown()
    {
        AppHost.Dispose();
    }
}

В вашей реальной реализации ServiceStack, вероятно, есть класс AppHost, который является подклассом AppHostBase (по крайней мере, если он работает в IIS). Нам нужно подклассифицировать другой базовый класс для запуска этого хоста ServiceStack в процессе:

// the main detail is that this uses a different base class
public class ServiceTestAppHost : AppHostHttpListenerBase
{
    public const string BaseUrl = "http://localhost:8082/";

    public override void Configure(Container container)
    {
        // Add some request/response filters to set up the correct database
        // connection for the integration test database (may not be necessary
        // depending on your implementation)
        RequestFilters.Add((httpRequest, httpResponse, requestDto) =>
        {
            var dbContext = MakeSomeDatabaseContext();
            httpRequest.Items["DatabaseIntegrationTestContext"] = dbContext;
        });
        ResponseFilters.Add((httpRequest, httpResponse, responseDto) =>
        {
            var dbContext = httpRequest.Items["DatabaseIntegrationTestContext"] as DbContext;
            if (dbContext != null) {
                dbContext.Dispose();
                httpRequest.Items.Remove("DatabaseIntegrationTestContext");
            }
        });

        // now include any configuration you want to share between this 
        // and your regular AppHost, e.g. IoC setup, EndpointHostConfig,
        // JsConfig setup, adding Plugins, etc.
        SharedAppHost.Configure(container);
    }
}

Теперь у вас должен быть запущен сервисный сервис ServiceStack для всех ваших тестов. Отправка запросов на эту услугу теперь довольно проста:

[Test]
public void MyTest()
{
    // first do any necessary database setup. Or you could have a
    // test be a whole end-to-end use case where you do Post/Put 
    // requests to create a resource, Get requests to query the 
    // resource, and Delete request to delete it.

    // I use RestSharp as a way to test the request/response 
    // a little more independently from the ServiceStack framework.
    // Alternatively you could a ServiceStack client like JsonServiceClient.
    var client = new RestClient(ServiceTestAppHost.BaseUrl);
    client.Authenticator = new HttpBasicAuthenticator(NUnitTestLoginName, NUnitTestLoginPassword);
    var request = new RestRequest...
    var response = client.Execute<ResponseClass>(request);

    // do assertions on the response object now
}

Обратите внимание, что вам может потребоваться запустить Visual Studio в режиме администратора, чтобы служба успешно открыла этот порт; см. комментарии ниже и этот следующий вопрос.

Далее: проверка схемы

Я работаю над API для корпоративной системы, где клиенты платят много денег за заказные решения и ожидают высокопрофессионального обслуживания. Таким образом, мы используем проверку схемы, чтобы быть абсолютно уверенным, что мы не нарушаем контракт на обслуживание на самом низком уровне. Я не думаю, что валидация схемы необходима для большинства проектов, но вот что вы можете сделать, если хотите продолжить свое тестирование.

Одним из способов ненадлежащего расторжения вашего контракта на обслуживание является изменение DTO способом, который не имеет обратной совместимости: например, переименуйте существующее свойство или измените код пользовательской сериализации. Это может сломать клиента вашей службы, поскольку данные больше не доступны или не обрабатываются, но вы, как правило, не можете обнаружить это изменение, проверяя свою бизнес-логику. Лучший способ предотвратить это - сохранить ваши запросы DTO отдельными и одноцелевыми и отдельными от вашего уровня доступа к бизнесу/данным, но все же есть шанс, что кто-то будет неверно применяйте рефакторинг.

Чтобы предотвратить это, вы можете добавить проверку схемы в свой функциональный тест. Мы делаем это только для конкретных случаев использования, которые мы знаем, что платный клиент фактически собирается использовать в производстве. Идея состоит в том, что если этот тест ломается, то мы знаем, что код, который нарушил тест, нарушит эту интеграцию с клиентами, если он будет развернут для производства.

[Test(Description = "Ticket # where you implemented the use case the client is paying for")]
public void MySchemaValidationTest()
{
    // Send a raw request with a hard-coded URL and request body.
    // Use a non-ServiceStack client for this.
    var request = new RestRequest("/service/endpoint/url", Method.POST);
    request.RequestFormat = DataFormat.Json;
    request.AddBody(requestBodyObject);
    var response = Client.Execute(request);
    Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
    RestSchemaValidator.ValidateResponse("ExpectedResponse.json", response.Content);
}

Чтобы проверить ответ, создайте файл JSON Schema, в котором описывается ожидаемый формат ответа: какие поля должны существовать для этот конкретный вариант использования, какие типы данных ожидаются и т.д. В этой реализации используется парсер схемы Json.NET.

using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema;

public static class RestSchemaValidator
{
    static readonly string ResourceLocation = typeof(RestSchemaValidator).Namespace;

    public static void ValidateResponse(string resourceFileName, string restResponseContent)
    {
        var resourceFullName = "{0}.{1}".FormatUsing(ResourceLocation, resourceFileName);
        JsonSchema schema;

        // the json file name that is given to this method is stored as a 
        // resource file inside the test project (BuildAction = Embedded Resource)
        using(var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceFullName))
        using(var reader = new StreamReader(stream))
        using (Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceFileName))
        {
            var schematext = reader.ReadToEnd();
            schema = JsonSchema.Parse(schematext);
        }

        var parsedResponse = JObject.Parse(restResponseContent);
        Assert.DoesNotThrow(() => parsedResponse.Validate(schema));
    }
}

Вот пример файла схемы json. Обратите внимание, что это относится только к одному варианту использования и не является общим описанием класса DTO ответа. Все свойства отмечены как необходимо, так как это те, которые ожидаются клиентом в этом случае использования. В схеме могут отсутствовать другие неиспользуемые свойства, которые в настоящее время существуют в ответном DTO. На основе этой схемы вызов RestSchemaValidator.ValidateResponse завершится неудачно, если в ответе JSON отсутствуют какие-либо из ожидаемых полей, появятся неожиданные типы данных и т.д.

{
  "description": "Description of the use case",
  "type": "object",
  "additionalProperties": false,
  "properties":
  {
    "SomeIntegerField": {"type": "integer", "required": true},
    "SomeArrayField": {
      "type": "array",
      "required": true,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "Property1": {"type": "integer", "required": true},
          "Property2": {"type": "string", "required": true}
        }
      }
    }
  }
}

Этот тип теста должен быть написан один раз и никогда не будет изменен, если только используемый пример использования не станет устаревшим. Идея состоит в том, что эти тесты будут представлять фактические применения вашего API в процессе производства и гарантировать, что точные сообщения, возвращаемые API promises, не изменятся таким образом, что нарушают существующие обычаи.

Дополнительная информация

В ServiceStack есть несколько примеров выполнения тестов против хоста in-process, на котором основана эта реализация.