Всем привет! Хабраюзер shai_xylyd написал статью про аспекты тестирования, где им были рассмотрены некоторые понятия и ценности TDD. В частности, он упомянул очень интересный способ создания первичных юнит-тестов — когда функциональный код пишется совместно с кодом юнит-теста, чем меня очень заинтриговал.
Дело в том, что я (как программист), нахожусь в состоянии переходного процесса между «классической» разработкой и разработкой test-driven, поэтому всякими способами ищу возможности упростить и сделать более естественной последнюю. После пары приседаний, сразу включиться в методику shai_xylyd не сумел. Начал переписку с автором статьи, где он натолкнул меня на мысль подойти к решению с математической точки зрения. Идея в том, чтобы воспользоваться функциональным пространством среды программирования и «разложить» написание юнит-теста на составляющие. После чего сделать выводы.
Для начала пара определений.
Первичный юнит-тест — блок кода, покрывающий «основную» функцию тестируемой сущности.
Вторичный юнит-тест — блок кода, покрывающий «основную» функцию тестируемой сущности в граничных условиях.
Пространство
(Другими словами, это множество существующих инстансов любых типов данных фиксированной платформы разработки).
Функцией
(Пример — в частном случае
Мне нужно определить первичный юнит-тест (далее просто тест).
Пусть
Если перевести на псевдокод, то тестом будет являться следующая последовательность псевдокода:
Пусть
Согласно описанному в самом начале методу, я должен писать функциональный код совместно с кодом теста. То есть:
Теперь очень важная выкладка: природа моков такова, чтобы поставлять тестовые данные неизменными. Т.е. сущность мокового объекта в том, чтобы подменить поведение зависимости в тесте, выдав определенный программистом набор тестовых данных. Иными словами,
На каждом этапе тест должен выполнятся успешно. Что важно, сохраняется свойство TDD — сначала пишем тест для сущности, потом саму сущность.
Опробую этот метод накошках себе. Платформа — .net, язык — C#, тестовая площадка — NUnit 2.x + Rhino Mocks 3.x
Задача следующая. Есть топология заводов. Нужно определить микросервис, который по идентификатору завода возвращает инстанс класса «Завод»:
За данные топологии отвечает сервис
Т.е. мне надо создать сервис, который имеет зависимость от
Шаг 1. Нужно определить тестовые данные и результирующее значение
Шаг 2. Определяем функциональность разрабатываемой сущности (по идентификатору завода получить объект «завод»)
Шаг 3. Создаем моки
Шаг 4: Рефакторинг и создание сущности:
Разработанная сущность
Пожалуй, наиболее очевидным преимуществом предложенного метода перед обычным методом написания тестов (когда сначала выполняется 4, а потом реализуется
Спасибо за внимание.
Дело в том, что я (как программист), нахожусь в состоянии переходного процесса между «классической» разработкой и разработкой test-driven, поэтому всякими способами ищу возможности упростить и сделать более естественной последнюю. После пары приседаний, сразу включиться в методику shai_xylyd не сумел. Начал переписку с автором статьи, где он натолкнул меня на мысль подойти к решению с математической точки зрения. Идея в том, чтобы воспользоваться функциональным пространством среды программирования и «разложить» написание юнит-теста на составляющие. После чего сделать выводы.
Теория
Для начала пара определений.
Первичный юнит-тест — блок кода, покрывающий «основную» функцию тестируемой сущности.
Вторичный юнит-тест — блок кода, покрывающий «основную» функцию тестируемой сущности в граничных условиях.
Пространство
Rp
— конечное множество существенных данных среды программирования. (Другими словами, это множество существующих инстансов любых типов данных фиксированной платформы разработки).
Функцией
f(x) : Rp -> Rp
назовем некоторую последовательность кода, выполненную над данными x
из Rp
. (Пример — в частном случае
f(x)
это простой метод класса, который принимает на вход x
. Если сказать еще грубее, то f
— это просто строчки кода).Мне нужно определить первичный юнит-тест (далее просто тест).
Пусть
z = h(x)
, где h
— функция теста. Зафиксируем какое-то значение xo
, тогда zo = h(xo)
. Теперь определим функцию a(zo)
, которая возвращает 0 (если zo
некорректно) или 1 (если zo
корректно). Иными словами, мы взяли какие-то тестовые данные xo
, совершили с ними какие-то манипуляции в виде h(xo)
и получили zo
. Потом мы сделали assert
для полученных данных zo
и проверили правильность теста.Если перевести на псевдокод, то тестом будет являться следующая последовательность псевдокода:
def xo
zo = h(xo)
a(zo)
Пусть
f(x)
— функциональный код (тот, который будет работать в разрабатываемой сущности). Согласно описанному в самом начале методу, я должен писать функциональный код совместно с кодом теста. То есть:
z = h(x) = f(m(x))
, где m(x)
— вспомогательный код: объекты-заглушки зависимостей функционального кода, моковые структуры фреймворка и т.п. (далее m(x)
— моки)Теперь очень важная выкладка: природа моков такова, чтобы поставлять тестовые данные неизменными. Т.е. сущность мокового объекта в том, чтобы подменить поведение зависимости в тесте, выдав определенный программистом набор тестовых данных. Иными словами,
m(x) = x
. Отсюда следует разделение f(m(x)) = f(x)
. Последнее позволяет четко описать алгоритм создания теста, где функциональный код разрабатывается совместно с кодом теста.Алгоритм
- Определение тестовых данных и проверка результатов
def xo
def zo
a(zo)
- Создание функционального кода
def xo
zo = f(xo)
a(zo)
- Создание моков и вспомогательного кода теста
def xo
zo = f(m(xo))
a(zo)
- Рефакторинг — вынесение
f(x)
в разрабатываемую сущность
def xo
zo = h(xo)
a(zo)
На каждом этапе тест должен выполнятся успешно. Что важно, сохраняется свойство TDD — сначала пишем тест для сущности, потом саму сущность.
Практика и примеры
Опробую этот метод на
Задача следующая. Есть топология заводов. Нужно определить микросервис, который по идентификатору завода возвращает инстанс класса «Завод»:
/// <summary>
/// Сервис определения завода по идентификатору
/// </summary>
/// <remarks>
/// В случае, если в срезе данных нет таблиц с заводом
/// </remarks>
public interface INodeResolver
{
/// <summary>
/// Найти завод по идентификатору
/// </summary>
/// <param name="id">Идентификатор завода</param>
/// <returns>Завод</returns>
Node FindById(int id);
}
* This source code was highlighted with Source Code Highlighter.
За данные топологии отвечает сервис
ITopologyService
:/// <summary>
/// Сервис данных топологии
/// </summary>
public interface ITopologyService
{
/// <summary>
/// Возвращает "топологию" системы (БСУ, Контроллеры, БСО, Линии, etc)
/// </summary>
DataSets.TopologyData GetTopology(IDataFilter filter);
}
* This source code was highlighted with Source Code Highlighter.
Т.е. мне надо создать сервис, который имеет зависимость от
ITopologyService
, получает от него данные и по переданному идентификатору создает новый инстанс класса Node
.Создание теста
Шаг 1. Нужно определить тестовые данные и результирующее значение
[Test]
public void FindNodeByIdTest()
{
// x0
TopologyData data = new TopologyData();
data.Node.AddNodeRow(1, "Завод1", Guid.NewGuid());
// z0
Node node = new Node { Id = 1, Name = "Завод1" };
Assert.AreEqual(1, node.Id);
Assert.AreEqual("Завод1", node.Name);
}
* This source code was highlighted with Source Code Highlighter.
Шаг 2. Определяем функциональность разрабатываемой сущности (по идентификатору завода получить объект «завод»)
[Test]
public void FindNodeByIdTest2()
{
// x0
TopologyData data = new TopologyData();
data.Node.AddNodeRow(1, "Завод1", Guid.NewGuid());
// f(x0)
TopologyData.NodeRow nodeRow = data.Node.FindByID(1);
// z0
Node node = new Node { Id = 1, Name = nodeRow.Description };
Assert.AreEqual(1, node.Id);
Assert.AreEqual("Завод1", node.Name);
}
* This source code was highlighted with Source Code Highlighter.
Шаг 3. Создаем моки
[Test]
public void FindNodeByIdTest3()
{
MockRepository repo = new MockRepository();
// x0
TopologyData data = new TopologyData();
data.Node.AddNodeRow(1, "Завод1", Guid.NewGuid());
// m(x0)
ITopologyService service = repo.StrictMock<ITopologyService>();
service.Expect(x => x.GetTopology(EmptyFilter.Instance)).Return(data).Repeat.Once();
repo.ReplayAll();
// f(m(x0)) = f(x0)
TopologyData dataSet = service.GetTopology(EmptyFilter.Instance);
TopologyData.NodeRow nodeRow = dataSet.Node.FindByID(1);
repo.VerifyAll();
// z0
Node node = new Node { Id = 1, Name = nodeRow.Description };
Assert.AreEqual(1, node.Id);
Assert.AreEqual("Завод1", node.Name);
}
* This source code was highlighted with Source Code Highlighter.
Шаг 4: Рефакторинг и создание сущности:
[Test]
public void FindNodeByIdTest4()
{
MockRepository repo = new MockRepository();
// x0
TopologyData data = new TopologyData();
data.Node.AddNodeRow(1, "Завод1", Guid.NewGuid());
// m(x0)
ITopologyService service = repo.StrictMock<ITopologyService>();
service.Expect(x => x.GetTopology(EmptyFilter.Instance)).Return(data).Repeat.Once();
repo.ReplayAll();
NodeResolver resolver = new NodeResolver(service);
// z0
Node node = resolver.FindById(1);
repo.VerifyAll();
Assert.AreEqual(1, node.Id);
Assert.AreEqual("Завод1", node.Name);
}
* This source code was highlighted with Source Code Highlighter.
Разработанная сущность
NodeResolver
получилась такой:/// <summary>
/// Сервис определения завода
/// </summary>
public class NodeResolver : INodeResolver
{
public NodeResolver(ITopologyService topologyService)
{
Guard.ArgumentNotNull(topologyService, "service");
_data = topologyService.GetTopology(EmptyFilter.Instance);
}
#region INodeResolver Members
public Node FindById(int id)
{
// f(x)
return new Node { Id = id, Name = _data.Node.FindByID(id).Description };
}
#endregion
private TopologyData _data;
}
* This source code was highlighted with Source Code Highlighter.
Выводы
Пожалуй, наиболее очевидным преимуществом предложенного метода перед обычным методом написания тестов (когда сначала выполняется 4, а потом реализуется
f(x)
), является экономия времени и «размазанность» разработки. Программисту теперь не приходится тратить время на код, который непосредственно не относится к функциональности программы. Он пишет код совместно с тестом, делая из двух зайцев одного (сам рефакторинг — это o-малое, которое можно отбросить).Спасибо за внимание.