среда, 14 сентября 2011 г.

BetTeamsBattle - Kick bookmaker's ass

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

Обсуждение выигрыша джекпота мне было не особенно интересно: все-таки когда речь заходит о трехзначных коэффициентах, это уже больше лотерея, нежели букмекерские ставки. Но в какой-то момент речь зашла о букмекерских ставках как об альтернативе банковским вкладам. Ознакомившись с позитивным опытом одного из соседей о накоплении процентов, многократно превосходящих те, что предлагают банки (в районе инфляции) за счет ставок, строго соответствовавших рекомендациям одного из прогнозистов, я заинтересовался этой темой. На волне интереса к этому вопросу, я решил создать небольшой сервис, который бы объединял профессиональных прогнозистов, а также давал возможность любителям посоревноваться наравне с профессионалами - так появился betteamsbattle.com.

Почему Bet Teams Battle?

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

В результате я решил использовать более абстрактное название Bet Teams Battle, а не HandiCappers Battle. Не уверен, насколько это правильное решение, и с удовольствием выслушаю ваше мнение на этот счет в комментариях. Кстати, сервис доступен еще по двум адресам: cappersbattle.com и handicappers.com, и я все еще рассматриваю вариант сделать один из них основным.

Суть проекта

Пользователь делает ставки в рамках турнира:

После того, как ставка сделана, она появляется в списке ставок:

После того, как событие произошло, необходимо выбрать его исход.

Как в момент открытия ставки, так и в момент ее закрытия делается скриншот страницы сайта букмекера, который выступает в роли доказательства. На основании ставок формируется статистика отдельного пользователя и общий рейтинг команд. Вот, вкратце, и все.


До первого турнира осталось чуть меньше недели. Все это время буду общаться с капперами и агитировать их принять участие в турнире. Что из этого выйдет - узнаем через неделю.

Исходный код проекта открыт и лежит на Google Code, а в этой гугл-группе можно обсудить вещи, связанные с разработкой. Если хотите потыкать в меня палкой за говнокод, то сделать это можно в этой ветке, а если хотите присоединиться к разработке проекте, то в этой.

Ближайшие планы по разработке:
1. Проработка системы турниров
2. Работа над упрощением/автоматизацией открытия и закрытия ставок
3. Реализация поддержки пользовательских турниров
4. Улучшение реализации статистики

P. S. Чтобы можно было поиграться с проектом до старта первого турнира, я поднял своего рода зеркало по адресу test.betteamsbattle.com.

суббота, 16 июля 2011 г.

Интеграционные vs. Unit-тесты

Три основных подхода касательно взаимоотношения интеграционных и unit-тестов:

1. Integration-driven. Изначально делаем тест интеграционным, используя реальные реализации зависимостей. При необходимости что-нибудь застабить/замокать подменяем необходимые зависимости на (внимание!) стабы/моки соответственно.
2. Unit-driven. Изначально делаем юнит-тест, а значит стабим/мокаем все зависимости. При необходимости заменяем некоторые зависимости реальными реализациями
3. Mixed. Основую логику тестируем unit-тестами, но пишем один-два интеграционных теста на основные сценарии.

Пост навеян вопросом на StackOverflow. Мне особенно симпатичен ответ ThomasArdal (который за mixed-вариант): хоть TDD и способствует использованию второго подхода, ничто не мешает в конце написать интеграционный тест. К сожалению, я пока в основном использую первый подход (медленный, но надежный), но потихоньку буду переходить к третьему.

А что по этому поводу думают мои дорогие читатели?

среда, 13 июля 2011 г.

понедельник, 6 июня 2011 г.

Стабить или не стабить репозитории?

Для тестировании кода, который использует репозитории, есть два основных подхода:

1. Стабить и мокать вызовы репозиториев
2. Использовать реальные реализации репозиториев (а значит работать с базой)

Какой из вариантов выбрать - большой вопрос и не меньший повод для холивара.

Возьмем для примера следующий сервис:

public class ParametersService : IParametersService
{
private readonly IParametersRepository _parametersRepository;

public ParametersService(IParametersRepository parametersRepository)
{
_parametersRepository = parametersRepository;
}

public Parameter GetParameterOrAddNew(string parameterName)
{
var parameter = _parametersRepository.GetParameterOrDefault(parameterName);
if (parameter == null)
parameter = _parametersRepository.Add(parameterName);
return parameter;
}
}


А теперь напишем тест (возьмем для примера NUnit и RhinoMocks) для первого:


private IParametersService _parametersService;

public void SetUp(IParametersRepository parametersRepository)
{
_parametersService = new ParametersService(parametersRepository);
}

[Test]
public void GetParameterOrDefault_NoParameter_ParameterIsAdded()
{
//создаем Stub + Mock
var parametersRepository = MocksRepository.GenerateMock<IParametersRepository>();
//Эмулируем отсутствие параметра
parametersRepository.Stub(pr => pr.GetParameterOrDefault("testParameterName")).Return(null);

SetUp(parametersRepository);

var parameter = _parametersService.GetParameterOrAddNew("testParameterName");

//Проверяем, что метод Add был вызван
parametersRepository.AssertWasCalled(pr => pr.Add("testParameterName"));
}

и второго:


private IParametersService _parametersService = new ParametersService(new ParametersRepository);

[Test]
public void GetParameterOrDefault_NoParameter_ParameterIsAdded()
{
var parameter = _parametersRepository.GetParameterOrDefault("testParameterName");
//Проверяем, что такого параметра нет
Assert.IsNull(parameter);

var parameter = _parametersService.GetParameterOrAddNew("testParameterName");

Assert.IsNotNull(parameter);
Assert.AreEqual("testParameterName", parameter.Name);
}


Какую из реализаций выбрать?

Первая реализация более "чистая": мы тестируем непосредственно логику сервиса, не отвлекаясь на сторонние зависимости - классический unit-тест.

Во втором же подходе тесты интеграционные: в одном тесте мы тестируем как логику сервиса, так и репозитории. Это хорошо или плохо? Во многом ответ на этот вопрос зависит от того, что из себя представляют ваши репозитории.

В .NET распространены два подхода к реализации паттерна репозиторий:
1 (классический). Инкапсуляция запроса в рамках метода репозитория. Методы репозитория возвращают сущность/коллекцию сущностей.
2. Легковесный репозиторий, который возвращает IQueryable, который, в свою очередь, уже вне репозитория обрастает фильтрами, сортировками, пейджингом и т. д. посредством паттерна Specification

С первой реализацией репозитория отлично сочетается первый подход к тестированию. Раз логика репозитория инкапсулирована в одном методе, мы можем написать к нему соответствующие интеграционные тесты (которые будут работать с базой данных), а затем с уверенностью стабить/мокать репозиторий.

Вторая же реализация больше подходит ко второму подходу к тестированию. Во-первых, раз мы возвращаем IQueryable, то находимся в зоне действия не LINQ To Objects, а LINQ To Entities, LINQ To NHibernate и т. д. Если же мы застабим репозиторий, то будем работать с LINQ To Objects, а значит наш код будет тестировать не то поведение, которое будет в продакшене. Во-вторых, ввиду того, что репозиторий не возвращает конечный результат выполнения запроса (как в первой реализации), такую реализацию репозитория не очень удобно стабить.

Нельзя не отметить, что производительность второго подхода гораздо ниже. Но давайте не будем забывать, что это тесты, а не продакшен. Говоря о производительности, мы должны ответить всего на один вопрос: достаточно ли высока производительность для того, чтобы не тормозить работу программиста при прогонке тестов? Вопреки распространенному мнению, зачастую ответ на этот вопрос будет положительным. Ведь локально мы прогоняем тесты лишь для модуля/модулей, над которым непосредственно в данный момент работаем. А сделать так, чтобы тесты всех модулей не тормозили на build-сервере, - задача решаемая (ведь build-сервер - это мощный сервер с современным железом, а не старый списанный десктоп, да?). Если производительность второго подхода вас не устраивает, но использовать первый подход не хочется, можно попробовать использовать in-memory базы данных: они практически выравнивают производительность двух подходов.

Производительность - не единственная проблема второго подхода: необходимо решить вопрос очистки базы данных после каждого теста. Для этого можно и накатывать после каждого теста базу (медленно решение), и откатываться к некоторому snapshot'у (работает быстрее, но поддерживается не всеми СУБД), и использовать транзакций без их последующего коммита. Я предпочитаю последний вариант за его универсальность и удовлетворяющую моим требованиям произвидительность. К тому же, благодаря TransactionScope реализовать этот подход очень просто. Правда, как я отмечал в своей недавней статье про TransactionScope, этот подход не работает, если в коде используется TransactionScope в режиме RequiresNew или Suppress. Заинтересовавшимся вопросом восстановления состояния базы данных, рекомендую серию постов на английском языке: раз, два, три

Вот, на мой взгляд, основные аргументы в пользу одного либо другого подхода к тестированию репозиториев.

P. S. Выше я изложил, почему с классическими репозиториями имеет смысл использовать первый подход к тестированию. Но при этом в своем личном проекте я тестирую такие репозитории вторым подходом. Просто мне спится спокойней, когда сервисы работают с реальными репозиториями. Как думаете, это паранойя? :)

PP. S. Добавил кросспост на Хабр: http://habrahabr.ru/blogs/net/120755/