Pull to refresh

Оптимизация процесса создания unit-тестов

Reading time 6 min
Views 4.7K
Всем привет! Хабраюзер shai_xylyd написал статью про аспекты тестирования, где им были рассмотрены некоторые понятия и ценности TDD. В частности, он упомянул очень интересный способ создания первичных юнит-тестов — когда функциональный код пишется совместно с кодом юнит-теста, чем меня очень заинтриговал.

Дело в том, что я (как программист), нахожусь в состоянии переходного процесса между «классической» разработкой и разработкой 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). Последнее позволяет четко описать алгоритм создания теста, где функциональный код разрабатывается совместно с кодом теста.

Алгоритм


  1. Определение тестовых данных и проверка результатов

    def xo
    def zo
    a(zo)

  2. Создание функционального кода

    def xo
    zo = f(xo)
    a(zo)

  3. Создание моков и вспомогательного кода теста

    def xo
    zo = f(m(xo))
    a(zo)

  4. Рефакторинг — вынесение f(x) в разрабатываемую сущность

    def xo
    zo = h(xo)
    a(zo)



На каждом этапе тест должен выполнятся успешно. Что важно, сохраняется свойство TDD — сначала пишем тест для сущности, потом саму сущность.

Практика и примеры


Опробую этот метод на кошках себе. Платформа — .net, язык — C#, тестовая площадка — NUnit 2.x + Rhino Mocks 3.x

Задача следующая. Есть топология заводов. Нужно определить микросервис, который по идентификатору завода возвращает инстанс класса «Завод»:

/// <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-малое, которое можно отбросить).

Спасибо за внимание.
Tags:
Hubs:
+14
Comments 17
Comments Comments 17

Articles