Статьи

.NET к Ruby: обучение написанию тестов, часть I

Если вы — разработчик .NET, который писал тесты, этот пост может побудить вас продолжать делать это при работе с Ruby. Если вместо этого вы не пишете тесты, мы должны это изменить! Я знаю, что упоминал об этом раньше, но не могу не подчеркнуть, насколько это важно.

В первые годы работы на C # я слишком полагался на компилятор. У меня была идея, что если компилятор не будет жаловаться, то мой код будет верным. В то время я «писал код, удовлетворяющий компилятору» , а не «создавал приложения, удовлетворяющие моих клиентов» . Так как я перешел из одного в другой? Я могу заверить вас, что этого не произошло за ночь, так же, как я могу заверить вас, что я очень рад каждый раз, когда вижу, что мой бекон спасен тестами, которые у меня есть на месте.

Я опубликую этот пост в двух частях: часть 1 посвящена моему опыту тестирования в .NET, а часть 2 — моему опыту работы с Ruby. Хотя весь код в части 1 написан на C #, некоторые части выглядят очень по-рубински, как вы, вероятно, поймете, когда перейдете к части 2.

Начиная с простых утверждений

Мой первый опыт с тестами был с написанием того, что я считал «модульными тестами». В то время я действительно просто писал некоторые утверждения, чтобы убедиться, что определенные методы для объектов возвращают то, что я ожидал. Что-то в этом роде:

[TestClass] public class CalculatorTests { [TestMethod] public void Test1() { var calculator = new Calculator(); var result = calculator.Sum(1, 1); Assert.AreEqual(2, result); } [TestMethod] public void Test2() { var calculator = new Calculator(); var result = calculator.Subtract(5, 3); Assert.AreEqual(2, result); } } 

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

В то время я не очень хорошо организовывал свой тестовый код: я обычно Test2 такие методы, как Test2 и Test2 , которые на самом деле не говорили мне, что тестируется. В тестовом коде всегда была избыточность, и один и тот же класс тестировался снова и снова.

Лучше заботиться о моем тестовом коде

Потратив некоторое время на написание множества подобных тестов, я заметил, что код был грязным и вышел из-под контроля, когда мне пришло время его поддерживать. Я понял, что должен написать лучший тестовый код. Я начал применять методы рефакторинга к своему тестовому коду: вводить переменные или константы и исключать магические значения, извлекать избыточный код в отдельные методы, именовать методы после их намерения и т. Д.

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

 [TestMethod] public void Sum_should_return_the_two_given_numbers_added_together() { ... } 

Мне понравилось это, потому что, если тест не пройден, сообщение об ошибке будет содержать метод теста, который не прошел, и я быстро посмотрю, какое требование было нарушено.

Когда C # 3.0 представил методы расширения , я последовал за крутыми ребятами и создал свои «тестовые расширения» с утверждениями, чтобы мои тесты выглядели так:

 [TestMethod] public void Sum_should_return_the_two_given_numbers_added_togeter() { var calculator = new Calculator(); var result = calculator.Sum(1, 1); result.ShouldBe(2); } 

Вы видите, как и многие другие, чтение чего-либо в коде, например, Assert.AreEqual (2, результат) кажется нелогичным; это почти читается как «утверждают, равны результату». Конечно, как программисты, мы склонны анализировать подобный код и извлекать из него какой-то смысл. Честно говоря, мне лучше не делать этот мысленный анализ, а просто читать код следующим образом: result.ShouldBe (2) . Чувак, просто позволь мне использовать мой крошечный мозг для чего-то другого.

Кстати, в случае, если вы еще не попали в методы расширения, вот как можно создать метод ShouldBe:

 public static class TestExtensions { public static void ShouldBe(this object expected, object actual) { Assert.AreEqual(expected, actual); } } 

Как только тестовый код выглядел чистым, имена моих тестовых методов начали несколько отражать требования, и мой тестовый код мог быть прочитан без большого умственного анализа, я начал использовать его вместо документации: если мне нужно было помнить, как определенные части код должен был быть использован, я бы прочитал тесты.

Переход к тест-ориентированной разработке

Прошло несколько месяцев, и я слышал, как люди говорили о разработке через тестирование (TDD). Идея написания тестов перед написанием кода была мне совершенно чужой. Я должен был заставить себя попробовать это, даже когда это казалось бессмысленным. Практика выросла на мне, и я понял, почему людям это понравилось.

Написание кода легко: мы можем использовать генераторы кода или генераторы микрокода (такие как CodeRush или Resharper), или фрагменты кода, или мастер Intellisense, или просто очень быстро печатать. Тем не менее, я закончил тем, что написал гораздо больше кода, чем было на самом деле необходимо. Вы знаете: «Я собираюсь разместить этот код здесь, потому что пользователю может понадобиться это в какой-то момент, или бизнес может потребовать этого, или, может быть, это, может быть, может быть, другой». Много предположений, которые много раз никогда не сбываются, в приложениях, которые никогда не видели свет дня.

Занимаясь TDD, я начал думать о «коде, который хотел иметь», когда писал тесты. Я научился бы более тщательно обдумывать, как я буду использовать объекты, которые собирался реализовать, или, что более важно, будет ли это иметь смысл для других разработчиков, пытающихся его использовать. Я бы попросил своих сверстников взглянуть на тесты и дать мне обратную связь, прежде чем я потратил время на реализацию классов. Я бы подумал о том, чтобы параметры передавались в методы: должен ли я передавать десятичную или строковую переменную или более явный тип, такой как Money или SSN?

Зная, что должны делать мои объекты, я начал больше думать только о написании достаточного количества кода, чтобы пройти тесты. Ни больше ни меньше. «Но что если / если / но / возможно». Мне все равно Если у меня будет больше вопросов, чем уверенности, я не буду просто изобретать и писать код, который смогу придумать. Этот код наверняка будет выброшен или, что еще хуже, загрязнит базу кода, когда он никому не нужен.

Следующий шаг: сосредоточиться на поведении

Мои первые шаги в Behavior Driven Development (BDD) были сосредоточены на поведении определенных объектов или компонентов моих приложений. После перехода назад и вперед между тестовыми средами «Given-When-Then» и «Context-Specification», для меня действительно хорошо работал SubSpec, где я мог писать тесты следующим образом:

 [Specification] public void dynamic_properties() { "Given additional dynamic data for a customer" .Context(() => make_data_for_a_customer()); "When constructing a customer object" .Do(() => { _customer = GetCustomer(); }); "Then the object should expose the additional data as properties" .Assert(properties_are_exposed); " And getters are available" .Assert(getters_are_exposed); " And setters are available" .Assert(setters_are_exposed); } 

Это все еще был только C #, но я бы начал с написания требований, просто используя простые английские предложения в форме «Учитывая некоторый контекст, когда что-то случается, тогда некоторое ожидание должно быть удовлетворено» . Это может звучать как мелочь, но это не так: вместо того, чтобы думать на C #, я думал на английском. Как только у меня появилась четкая идея о том, как я могу объяснить, что должен делать мой код, было гораздо проще написать, как должен выглядеть код.

Это стоило того?

Беда? Какие проблемы? Я могу вам сказать, что, пройдя через все эти итерации по улучшению написания моих тестов, я стал гораздо лучшим разработчиком:

  • Мои навыки в C # улучшились: я многое узнал о лямбдах, методах расширения, обобщениях и множестве других вещей в языке;
  • Мои навыки ООП улучшились: я узнал о принципах SOLID , как писать лучшие объекты, методы, абстракции и т. Д .;
  • Мои навыки английского языка улучшились: если я отбрасываю английские предложения в своем коде, они должны быть короткими и конкретными. Честно говоря, я думаю, что если я не смогу четко общаться на английском языке, мой код также пострадает, и мой код будет невыразительным.
  • Мои коммуникативные навыки улучшились: если я не могу поделиться своими мыслями и пониманием работы, которую мне нужно сделать, очень вероятно, что я буду создавать приложение, которое ни один пользователь не захочет использовать. Обладая лучшими навыками общения, у меня гораздо больше шансов тратить время только на то, что действительно нужно клиенту.

Так что без проблем? Хорошо, были некоторые проблемы. Он варьируется от использования неправильных инструментов, что делает написание тестов сложной задачей, до использования каркасов или компонентов, которые просто не дружественны к тестам (мы не можем тестировать наш код, потому что он использует некоторый класс, который не тестируется; по крайней мере, не в Простой способ).

Существует также культурная проблема: кажется, людям трудно понять, почему разработчик написал тест. По-видимому, это нормально тратить часы на отладчик, но не стоит тратить время на написание тестов. Действительно ли клиенты заботятся о том, написал ли разработчик блок if, блок переключателей, написал тест или провел время на отладчике?

Хорошо, я отвлекся …

Возвращаясь к вашему вопросу: Да, насколько я понимаю, это стоило больше никаких проблем. Во второй части этой серии статей я расскажу о том, как работа с тестами помогла мне сравнительно быстро начать работать с Ruby, а также о том, как некоторые инструменты в Ruby позволили мне улучшить методы разработки программного обеспечения. Увидимся там…