Тестирование тривиального кода

в 10:45, , рубрики: best practice, clean code, Mark Seemann, tdd, Программирование, разработка

Даже если код тривиален, вы всё равно должны его тестировать.
Пару дней назад, Роберт Мартин опубликовал пост «Прагматичность TDD», (здесь лежит переводприм.переводчика) где он рассказал о том, что не тестируют абсолютно весь код. Среди исключительных ситуаций, когда не стоит применять TDD, дядя Боб упоминает написание GUI-кода, и я вижу смысл в таких утверждениях, но среди исключений есть парочка, на мой взгляд, нелогичных.
Роберт Мартин утверждает, что не разрабатывает через тестирование:

  • геттеры и сеттеры;
  • однострочные функции;
  • абсолютно тривиальные функции.

По сути, эти утверждения сводятся к единственному правилу о том, чтобы не разрабатывать через тестирование «тривиальный» код, такой как геттеры и сеттеры (свойства для программистов .NET).
Есть несколько проблем, связанных с этим правилом применения TDD, которые я хотел бы обсудить:

  • здесь имеет место спутывание причины и следствия;
  • тривиальный код может измениться в будущем;
  • это ужасный совет для новичков.

Позвольте мне рассмотреть каждый из этих пунктов подробнее.

Причинно-следственная связь

Весь смысл TDD заключается в том, что тесты направляют реализацию. Тест – это причина, а реализация – это следствие. Если вы следуете этому принципу, то каким образом на планете Земля, вы можете решить не писать тест из-за того, что ожидаемая реализация будет тривиальной сложности? Вы ещё этого даже не знаете. Это логически невозможно.
Роберт Мартин сам предложил условие очерёдности преобразований (Transformation Priority Premise — TPP), и одна из мыслей заключается в том, что когда вы начинаете направлять разработку нового кода через тесты, вы должны стараться делать это мелкими формализованными шажками. Первый шажок, скорее всего, приведёт вас к тривиальной реализации, такой как возврат константы.
С точки зрения TPP, единственная разница между тривиальным и нетривиальным кодом это то, как далеко вы зашли в процесс направления реализации через тестирование. Таким образом, если «тривиальность» это всё, что вам требуется, единственно верным путём является написание единственного теста, который будет демонстрировать, что тривиальное поведение работает так, как и ожидается. Таким образом, согласно TPP, вы разделаетесь с задачей.

Инкапсуляция.

Исключение Роберта Мартина насчёт геттеров и сеттеров ставит в тупик в особенности. Если вы считаете тот или иной геттерсеттер (или .NET свойство) тривиальным, зачем вы вообще его имеете? Почему бы в таком случае не реализовать открытое поле в классе?
Существуют отличные причины для того, чтобы избегать реализации открытых полей класса, и все они связаны с инкапсуляцией. Данные должны быть реализованы через геттеры и сеттеры, потому что это даст возможность изменения их реализации в будущем.
Что именно мы называем процессом изменения реализации без изменения поведения?
Мы это называем рефакторингом. Откуда мы можем знать, что, изменяя код реализации, мы не изменим поведение?
Как утверждает Мартин Фаулер в книге «Рефакторинг», вы должны иметь хороший набор тестов в качестве сетки безопасности, иначе вы не будете знать, сломали ли вы что-то или нет.
Хороший код живёт и развивается долгое время. То, что в начале было тривиальным может измениться за долгий промежуток времени и вы не можете предсказать будут ли оставаться тривиальные члены тривиальными в течении нескольких лет. Важно быть уверенным в том, что тривиальное поведение остаётся корректным с добавлением к нему сложности. Регрессионный набор тестов призван решить эту проблему, но только в том случае, если вы реально пишете тесты на тривиальные фичи.
Роберт Мартин приводит в качестве аргумента то, что геттеры и сеттеры тестируются косвенно через другие тесты, но, несмотря на то, что это может быть верно на этапе объявления члена, не факт, что это будет верно сколь угодно долгое время. Спустя месяцы, эти тесты могут быть удалены, оставляя введённый тривиальный член непокрытым тестами.
Вы можете смотреть на это так: вы можете следовать или не следовать TPP с TDD, но для тривиальных членов, временной разрыв между первым и вторым преобразованиями может измеряться месяцами, а не минутами.

Изучение TDD

Я думаю, что быть прагматиком это хорошо, но «правило», говорящее о том, что вы не обязаны разрабатывать через тестирование «тривиальный» код, является просто ужасным советом для новичков. Если вы даёте кому-либо, изучающему TDD, путь, следуя которому, можно избежать этого самого TDD, то этот кто-то, сталкиваясь с какими-либо сложностями, будет идти этим путём всякий раз. Если уж вы предоставляете такой обходной путь, то должны, по крайней мере, сделать условия явными и измеряемыми.
Расплывчатое условие, звучащее «вы можете предугадать, что реализация будет тривиальной», абсолютно неизмеряемо. Вы можете думать, что знаете то, как будет выглядеть некая реализация, но, позволяя направлять реализацию через тесты, вы, зачастую, будете удивлены. Именно по такому пути мышления нас и направляет TDD – то, что вы первоначально думали, будет работать, работать не будет.

Анализ первопричины

Настаиваю ли я на том, что вы должны применять TDD относительно всех геттеров и сеттеров? Да, я действительно настаиваю.
Вы могли бы сказать, что такой подход будет отнимать слишком много времени. Роберт Мартин иронично подмечал по этому поводу:

«Единственный способ продвигаться быстро – продвигаться грамотно».

И всё же, давайте посмотрим, на что будет похоже применение TPP к свойствам (java-программисты могут продолжать читать. Свойства в C# — просто синтаксический сахар для геттеров и сеттеров).
Скажем, у меня есть класс DateViewModel и я хочу, чтобы у него было свойство типа int, представляющее год. Первый тест будет таким:

[Fact]
public void GetYearReturnsAssignedValue()
{
    var sut = new DateViewModel();
    sut.Year = 2013;
    Assert.Equal(2013, sut.Year);
}

Не принимая ничего на веру и во всём сомневаясь, правильной реализацией будет следующая:

public int Year
{
    get { return 2013; }
    set { }
}

Следуя TPP, этот код будет выглядеть именно так. Что ж, давайте напишем ещё один тест:

[Fact]
public void GetYearReturnsAssignedValue2()
{
    var sut = new DateViewModel();
    sut.Year = 2010;
    Assert.Equal(2010, sut.Year);
}

Вместе эти два теста побуждают меня реализовать свойство корректно:

public int Year { get; set; }

Несмотря на то, что эти два теста могут быть отрефакторены до одного параметризованного теста, проделанной работы-то всё равно очень много. Нам ведь придётся писать два теста не только для одного свойства, но и проделывать такое для каждого!
«Проделывать абсолютно то же самое?», — спросите вы. Да вы что, совсем что-ли?
Ох, ну вы же программист. Что делают программисты, когда им приходится делать одно и то же снова и снова?
Ну, если вы миритесь с противоречивыми правилами, которые позволяют вам избежать ранящей вас работы, то это неверный ответ. Если работа вас ранит – делайте её почаще.
Программисты автоматизируют повторяющиеся действия. Вы можете поступать также и в случае с тестированием свойств. Вот как я сделал тоже самое, используя AutoFixture:

[Theory, AutoWebData]
public void YearIsWritable(WritablePropertyAssertion a)
{
    a.Verify(Reflect<DateViewModel>.GetProperty<int>(sut => sut.Year));
}

Это декларативный способ проверки того же самого поведения, что и в предыдущих двух тестах, но длинной в одну строку.
Здесь уместен анализ первопричины. Складывается ощущение, что коэффициент затраты/выгоды, относительно применения TDD к геттерам и сеттерам очень высок. Однако, я думаю, что Роберт Мартин остановился на этом, потому что счёл затраты фиксированными, а выгоды слишком малыми. Однако, несмотря на то, что выгоды могут казаться незначительными, затраты не обязаны оставаться фиксировано высокими. Снизь затраты и коэффициент затраты/выгоды улучшиться. Вот почему вы всегда должны вести разработку геттеров и сеттеров через тестирование.

От переводчика: планирую выпустить в следующий раз перевод какого-либо обзора AutoFixture, либо сделать обзор самому. ИМХО, крайне интересный инструмент.

Автор: EngineerSpock

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js