Тестирование Web API
Особенности моего проекта
— Большая зависимость от БД
— Зависимость от внешних сервисов
• Push сообщения
• Email
• Сокращение ссылок
• Платежи
Webapi
Внешние
сервисыБД Внутренние
сервисы
Боль тестирования
— Тестирование контроллеров
• Много мокать
• Сложная поддержка
• Не тестируются запросы к БД
— Интеграционные тесты
• Малое покрытие
• Очень трудно тестировать сложные сценарии
— Ручное тестирование
• Долго
• Никто не делает регресс
Доклад про TestServer — озарение!
— Выглядит просто
— Решил попробовать
— Сделал предположения
• можно мочить
• можно дебажить в том же процессе (в одной студии)
Внедрение TestServer
— Готовая реализация под .Net Core, а у меня классический .Net
— nUnit
— Внедрял параллельно разработке
Предметная область демо-проекта
— Есть группы
— У групп есть пользователи
— У пользователей есть роли: владелец, юзер и модератор
— Владелец может назначать модераторов
— Владелец и модератор могут публиковать заметки
Структура тестов
Test API client Requester API server
[OneTimeSetUp]
public void OneTimeSetUp()
{
ApiServer = new ApiServer();
var requester = ApiServer.GetRequester();
ApiClient = new ApiClient(requester);
}
[OneTimeTearDown]
public void OneTimeTearDown()
{
ApiServer.Dispose();
}
[SetUp]
public void Setup()
{
ApiServer.Reset();
}
Test API client Requester API serverTest
— Администратор создаёт группу
— Пользователь вступает в группу
— Администратор назначает пользователя модератором
— Пользователь может создавать заметки
Test API client Requester API serverTest
Test API client Requester API serverTest
var adminToken = await ApiClient.Account.GetToken("123", "456");
var userToken = await ApiClient.Account.GetToken("aaa", "bbb");
var groupOwner = await ApiClient.Group.CreateGroup(adminToken, new CreateGroupDto
{
Name = "Тестовая группа " + DateTime.UtcNow.Ticks
});
GroupMember userMember = await ApiClient.Group.Join(userToken, groupOwner.Group.Id);
await ApiClient.Group.SetModerator(adminToken, groupOwner.Group.Id, userMember.Id);
var note = await ApiClient.Group.AddNote(
userToken, groupOwner.Group.Id, "Заметка от модератора");
Assert.That(note, Is.Not.Null);
Assert.That(note.Text, Is.EqualTo("Заметка от модератора"));
public class GroupApiClient
{
private readonly IHttpRequester _requester;
public GroupApiClient(IHttpRequester requester)
{
_requester = requester;
}
public async Task<GroupMemberDto[]> GetGroups(string token)
{
return await _requester.GetAsync<GroupMemberDto[]>("/api/group", token);
}
public async Task<GroupMemberDto> CreateGroup(string token, CreateGroupDto dto)
{
return await _requester
.PostAsync<CreateGroupDto, GroupMemberDto>("/api/group", token, dto);
}
Test API client Requester API serverAPI client
public HttpClient CreateClient(string token = null)
{
var client = new HttpClient(_handler) {BaseAddress = _baseAddress};
if (token != null)
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
return client;
}
public async Task<TResult> GetAsync<TResult>(string url, string token)
{
using (var client = CreateClient(token))
{
var response = await client.GetAsync(url);
await CheckStatusCode(response);
return await Deserialize<TResult>(response);
}
}
Test API client Requester API serverRequester
public class ApiServer : IDisposable
{
public TestServer TestServer { get; }
private TestStartup Startup { get; }
public ApiServer()
{
Startup = new TestStartup();
TestServer = TestServer.Create(app =>
{
Startup.Configuration(app);
});
}
public IHttpRequester GetRequester()
{
return new HttpRequester(TestServer.Handler, TestServer.BaseAddress);
}
public void Reset()
{
//Логика сброса моков перед каждым тестом
}
Test API client Requester API serverAPI server
public class TestStartup : Startup
{
protected override WindsorContainer CreateWindsorContainer()
{
var container = base.CreateWindsorContainer();
return container;
}
protected override IncludeErrorDetailPolicy GetErrorDetailPolicy()
=> IncludeErrorDetailPolicy.Always;
}
Test API client Requester API serverAPI server
Профит
— Тесты стали похожи на реальные пользовательские сценарии
— Отладка в одной студии
— Сокращение цикла отладки
— Внешние зависимости перестали мешать тестировать
— Уверенность в работоспособности системы увеличилась
Дальнейшее развитие
— Единожды создавать тест сервер в памяти
— Настроить запуск на TeamCity
— Мокать под конкретный тест
Спасибо за внимание!
Email: rusakov@byndyusoft.com
Vk: https://vk.com/id512458
GitHub: https://github.com/Mblkolo
Telegram: https://t.me/Mblkolo

Тестирование Web API

  • 1.
  • 2.
    Особенности моего проекта —Большая зависимость от БД — Зависимость от внешних сервисов • Push сообщения • Email • Сокращение ссылок • Платежи Webapi Внешние сервисыБД Внутренние сервисы
  • 3.
    Боль тестирования — Тестированиеконтроллеров • Много мокать • Сложная поддержка • Не тестируются запросы к БД — Интеграционные тесты • Малое покрытие • Очень трудно тестировать сложные сценарии — Ручное тестирование • Долго • Никто не делает регресс
  • 4.
    Доклад про TestServer— озарение! — Выглядит просто — Решил попробовать — Сделал предположения • можно мочить • можно дебажить в том же процессе (в одной студии)
  • 5.
    Внедрение TestServer — Готоваяреализация под .Net Core, а у меня классический .Net — nUnit — Внедрял параллельно разработке
  • 6.
    Предметная область демо-проекта —Есть группы — У групп есть пользователи — У пользователей есть роли: владелец, юзер и модератор — Владелец может назначать модераторов — Владелец и модератор могут публиковать заметки
  • 7.
    Структура тестов Test APIclient Requester API server
  • 8.
    [OneTimeSetUp] public void OneTimeSetUp() { ApiServer= new ApiServer(); var requester = ApiServer.GetRequester(); ApiClient = new ApiClient(requester); } [OneTimeTearDown] public void OneTimeTearDown() { ApiServer.Dispose(); } [SetUp] public void Setup() { ApiServer.Reset(); } Test API client Requester API serverTest
  • 9.
    — Администратор создаётгруппу — Пользователь вступает в группу — Администратор назначает пользователя модератором — Пользователь может создавать заметки Test API client Requester API serverTest
  • 10.
    Test API clientRequester API serverTest var adminToken = await ApiClient.Account.GetToken("123", "456"); var userToken = await ApiClient.Account.GetToken("aaa", "bbb"); var groupOwner = await ApiClient.Group.CreateGroup(adminToken, new CreateGroupDto { Name = "Тестовая группа " + DateTime.UtcNow.Ticks }); GroupMember userMember = await ApiClient.Group.Join(userToken, groupOwner.Group.Id); await ApiClient.Group.SetModerator(adminToken, groupOwner.Group.Id, userMember.Id); var note = await ApiClient.Group.AddNote( userToken, groupOwner.Group.Id, "Заметка от модератора"); Assert.That(note, Is.Not.Null); Assert.That(note.Text, Is.EqualTo("Заметка от модератора"));
  • 11.
    public class GroupApiClient { privatereadonly IHttpRequester _requester; public GroupApiClient(IHttpRequester requester) { _requester = requester; } public async Task<GroupMemberDto[]> GetGroups(string token) { return await _requester.GetAsync<GroupMemberDto[]>("/api/group", token); } public async Task<GroupMemberDto> CreateGroup(string token, CreateGroupDto dto) { return await _requester .PostAsync<CreateGroupDto, GroupMemberDto>("/api/group", token, dto); } Test API client Requester API serverAPI client
  • 12.
    public HttpClient CreateClient(stringtoken = null) { var client = new HttpClient(_handler) {BaseAddress = _baseAddress}; if (token != null) client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); return client; } public async Task<TResult> GetAsync<TResult>(string url, string token) { using (var client = CreateClient(token)) { var response = await client.GetAsync(url); await CheckStatusCode(response); return await Deserialize<TResult>(response); } } Test API client Requester API serverRequester
  • 13.
    public class ApiServer: IDisposable { public TestServer TestServer { get; } private TestStartup Startup { get; } public ApiServer() { Startup = new TestStartup(); TestServer = TestServer.Create(app => { Startup.Configuration(app); }); } public IHttpRequester GetRequester() { return new HttpRequester(TestServer.Handler, TestServer.BaseAddress); } public void Reset() { //Логика сброса моков перед каждым тестом } Test API client Requester API serverAPI server
  • 14.
    public class TestStartup: Startup { protected override WindsorContainer CreateWindsorContainer() { var container = base.CreateWindsorContainer(); return container; } protected override IncludeErrorDetailPolicy GetErrorDetailPolicy() => IncludeErrorDetailPolicy.Always; } Test API client Requester API serverAPI server
  • 15.
    Профит — Тесты сталипохожи на реальные пользовательские сценарии — Отладка в одной студии — Сокращение цикла отладки — Внешние зависимости перестали мешать тестировать — Уверенность в работоспособности системы увеличилась
  • 16.
    Дальнейшее развитие — Единождысоздавать тест сервер в памяти — Настроить запуск на TeamCity — Мокать под конкретный тест
  • 17.
    Спасибо за внимание! Email:rusakov@byndyusoft.com Vk: https://vk.com/id512458 GitHub: https://github.com/Mblkolo Telegram: https://t.me/Mblkolo

Editor's Notes

  • #3 Отрезать базу данных сложно, нужно очень много мокать, не будут тестироваться запросы, реальные изменения в бд Внние сервисы отрезать относительно не сложно
  • #4 Трудно тестировать сложные сценарии потому что есть интеграция с внешними сервисами. Можно сделать специальный флаг, но это нужно поддерживать.
  • #5 Предположение чем это мне полезно 15 мая был доклад от Димы
  • #6 Не просил у заказчика времени на внедрение, т.к. заказчик не хочет покупать QA. Делал вместе с фичами от минимального прототипа и по нарастающей Изучил Димину реализацию, получалось что так просто реализацию не скопировать, т.к. использовалось много специфичных классов. У нас уже использовался nUnit, переезд на xUnit потребовал бы времени
  • #8 Поток выполнения запроса. Тест обращается к апи клиенту, который через реквестер вызывает методы апи сервера.
  • #14 Настройки и моки
  • #15 Настройки и моки