Разработка через тестирование (TDD) и Delphi

No matter how slow you are writing clean code, you will always be slower if you make a mess.
— Uncle Bob Martin

Большое спасибо всем, кто принял участие в голосовании! Судя по результатам и комментариям — тема актуальна, и многие не используют автоматическое тестирование потому, что в Интернете очень мало информации по этому вопросу. Недостаток вводных статей по тестированию кода, на мой взгляд, в том, что они очень поверхностны. Читателю предлагают сферический пример калькулятора в вакууме, который с реальными проектами никак не связан.

В своей статье я постараюсь исправить ситуацию. Она разбита на несколько частей. В первой я расскажу об автоматическом тестировании и о главной ошибке на пути его внедрения, а также об очень мощном инструменте — разработке через тестирование. Вторая часть будет живым примером такой разработки, я поставлю небольшую реальную задачу и создам проект (с открытым кодом), описывая каждый шаг разработки. Т.к. чаще всего у многих из вас уже есть проекты, которые, естественно, вы не будете переписывать с нуля, я расскажу о том, как внедрять юнит-тесты на существующих проектах и как бороться с сопротивлением начальства и коллег. В завершение — несколько советов и рекомендаций.

Приятного чтения!

Краткое введение в юнит-тесты

Юнит-тест — это код, который тестирует код в автоматическом режиме (т.е. без участия пользователя). Unit в этом слово-сочетании означает то, что тест проверяет только одну, выделенную часть программы (например, функцию, процедуру или метод класса) в изоляции.

Самый простой пример: Вы пишете функцию, которая из XML достает нужные Вам данные. Можно создать новый проект с одной формой и одной кнопкой, по нажатию на которую Вы загрузите XML и передадите его в свою ф-ю, после чего проверите правильность результатов (которые увидите в MessageBox’е). Или напишете процедуру, в которой заранее подготовленный XML передадите своей функции и там же, в коде, проверите ожидаемый результат. Второй способ и будет юнит-тестом.

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

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

Нужно понимать, что идеально правильного теста не существует. Вспомните свой код, который казался Вам вершиной инженерной мысли год или два назад. Наверняка сейчас, набравшись больше опыта, Вы его таким уже не считаете. С тестами та же история — идеальный тест сегодня уже не будет таким для Вас завтра. Поэтому, не пытайтесь сделать все безупречно! Начинайте с простого! Экспериментируйте! Развивайте свой код и свои тесты итерациями, каждый раз улучшая их структуру и содержание. А если Вы сталкиваетесь с трудностями — почитайте советы опытных разработчиков, наверняка они через это уже прошли.

Также забудьте о 100% покрытии кода тестами (о котором так любят говорить задроты). Оно просто того не стоит. Здесь отлично работает правило 80/20. Всего 20% усилий сможет дать Вам проверку 80% Вашего кода (а это намного больше чем вообще ничего). К остальным 80% усилий, на мой взгляд, относится тестирование графического интерфейса. Если Вы построили приложение так, что UI используется только для передачи данных во внутрь приложения и для вывода результатов, достаточно протестировать только этот внутренний движок.

И немого информации для тех, кто считает тестирование слишком затратным (наверняка, такое ошибочное мнение складывается только из-за небольших трудностей, которые тяжело преодолеть думая о тестах как о догме). Написание теста действительно занимает больше времени, чем ручная проверка и исправление проблем через отладчик, прямо на месте. Но если посмотреть в перспективе — это очень выгодное вложение. Автоматический тест выполняется за доли секунд, ему никогда не лень и он всегда внимателен (в отличии от человека). Если одну и ту же часть программы Вы проверяете 5-7 раз вручную, уже на этом этапе тест бы себя окупил. А если подумать о регрессии (ошибка в одном месте ломает что-то в другом) тесты — незаменимая вещь.

STUBS & MOCKS

Для обеспечения тестирования в изоляции используется подмена внешнего кода заглушками.

Stub (стаб, заглушка) — это код, которым заменяется некоторый другой код (процедура, функция, класс), от которого зависит тестируемый код. Например, Ваш метод зависит от функции, которая делает HTTP запрос. Стабом будет замена этой функции, на другую, которая возвращает заранее известный результат (и не лезет в сеть).

Mock (мок) — это тот же стаб, но который используется для того, что бы проверить, вызывал ли его Ваш код. Например, Ваш код должен сделать несколько HTTP запросов посредством метода HttpPut. Вы заменяете HttpPut на мок и в конце тестов проверяете, сколько раз он был вызван. Мок очень удобен когда Ваш код должен вызвать другую подсистему, а не вернуть какое-то значение.

Разработка приложения с использованием TDD

TDD (Test Driven Development) — это разработка приложения через тестирование. Это такой метод разработки программы, при котором тест пишется ДО ТОГО, как написан код. И в этом весь смысл! TDD заставляет нас сначала подумать об интерфейсе кода (ведь не зная интерфейс, мы не можем ничего протестировать) а уже потом — о реализации.

Если взять пример с XML, то перед написанием нашего парсера мы не лезем в Google искать «как распарсить XML в Delphi» а думаем, как эта функция будет использоваться. Ее аргумент — строка или поток (TStream)? Если XML не корректный, возвращать 0, пустой список или генерировать Exception? Правильных ответов на эти вопросы нет, выбирайте то, что Вам подходит в рамках конкретного проекта. Вся суть в том, что вы формируете ожидания от кода до написания самого кода, ведь реализацию функции заменить будет очень просто, а ее интеграцию со всей системой — намного сложнее. Далее — по очень простой итеративной схеме:

1. Пишем тест
2. Запускаем тест — он должен провалится
3. Пишем код, что бы тест заработал
4. Рефакторинг кода
5. Повторяем

Вот и все! Очень просто и одновременно очень сложно (особенно если до этого Вы тесты не писали). Главное помнить, что идеального теста нет, и улучшать тесты вместе с кодом. Конечно, есть несколько советов (я их собрал в конце статьи), но и у них есть исключения.

Пишите разные тесты для одной и той же функции, по одному на каждый вариант использования. Важно писать тест ДО написания кода. В примере с XML создайте тест с корректным и некорректным XML, с пустыми данными, с очень большими данными и т.д. Думайте о тестах не как об отдельной сущности, а как об обратной стороне монеты Вашего кода.

Коротко о DUnit

В качестве инструмента для тестирования я выбрал DUnit, т.к. он уже интегрирован в Delphi. Тесты в DUnit группируются в тест-кейсы. TestCase — это наследник класса TestFramework.TTestCase, published методы которого будут рассматриваться как независимые Unit-тесты. Но перед тем, как наш тест-кейс попадет в общий список тестов для выполнения, его нужно зарегистрировать с помощью поцедуры TestFramework.RegisterTest(…), которую удобней всего вызывать в секции initializaition. Все тест-кейсы располагаются в отдельных unit-ах и подключаются к так называемому Test Project. Он представляет собой обычный Delphi-проект, но отличается тем, что в качестве главной формы создается форма от TestFramework (также возможно создать консольный вариант). Т.о. тесты будут выполняться как отдельный проект, который может быть запущен под отладчиком (debugger, breakpoints и т.д.) или (что намного удобнее и быстрее) через меню Run — Run Without Debugging.

Таким образом, один тест представляет собой published метод TestCase’a, в котором для проверки ожидаемых результатов используются специальные функции CheckTrue, CheckEquals, CheckNull и т.д.

SHOW ME THE CODE!

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

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

Итак, задача: написать приложение на Delphi, которое будет следить за указанным пользователем RSS-потоком, сохранять записи в локальное хранилище и показывать всплывающие подсказки на рабочем столе, если в тексте RSS-записи будут встречаться указанные пользователем слова. Приложение должно уметь сохранять свои настройки и показывать локально сохраненный список новостей.

Почему именно такой пример? Ну, во-первых, приложение может быть полезным (например, для слежением за новостями) а не абстрактным примером в вакууме. Во-вторых, в нем множество разных подсистем, их тестирование будет интересной задачей.

Подумайте, как бы такую задачу решали Вы? С чего бы начали?

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

1. Взаимодействие с web
2. Разбор XML
3. Хранилище для ленты новостей
4. Основная бизнес-логика
5. Пользовательский интерфейс (UI)

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

ИТЕРАЦИЯ №1

Начнем с создания 2х проектов — первый VCL Forms Application, второй Test Project. Очень удобно их хранить вместе как Delphi Project Group. Теперь выберем одну подсистему. Я начну с первой. Проанализировав задачу приложения, видим что взаимодействие с сетью ограничивается посылкой HTTP GET запроса на некоторый URL RSS-ленты и получение в ответ XML документа. Давайте напишем тест, в котором представим как нам хотелось бы использовать эту подсистему.

Можно использовать File — New — Other — Unit Test — TestCase, в этом случае Delphi предложит сгенерировать тест-кейс для существующего файла. Но ведь пока что у нас нет тестируемого кода, в этом и смысл TDD. Поэтому добавим пустой Delphi Unit, в uses-секцию внесем модуль TestFramework и создадим каркас своего первого теста.

unit TestHttpClient;
interface

uses
  TestFramework;

type
  THttpClientTest = class(TTestCase)
  published
    procedure ItGetsContentByURL;
  end;

implementation

{ THttpClientTest }

procedure THttpClientTest.ItGetsContentByURL;
begin

end;

initialization
  RegisterTest(THttpClientTest.Suite);

end.

Уже в таком виде можно запускать TestProject. Вы увидите главное окно DUnit с кнопкой запуска тестов. Наш первый тест не проходит, т.к. мы не делали в нем никаких проверок и DUnit считает такой тест пустым. Идею называть тесты «ItGetsContentByURL» я подсмотрел в мире Ruby. На мой взгляд, это очень удобно, имя метода уже говорит нам что делает тестируемый код.

Как будет выглядеть тестируемая функция? Давайте начнем с самого простого варианта (усложнить мы всегда успеем)

procedure THttpClientTest.ItGetsContentByURL;
var Response: string;
begin
  Response := GetPageByURL('http://delphi.frantic.im/feed/');
  CheckNotEquals(0, Pos('<rss', Response), 'Response doesnt contain RSS feed');
end;

Функции GetPageByURL пока что нет, но мы уже фиксируем в коде то, какой у нее должен быть интерфейс. По URL (строка) она должна вернуть строку с содержимым ресурса, причем синхронно (блокируя вызывающий поток). После получения ресурса надо проверить что мы получили именно то, что хотели. Проверка CheckNotEquals(0, Pos(‘<rss’, Response)) — не идеальный вариант, но выполняет свою задачу. Адепты юнит-тестов увидев этот тест немедленно бросят в меня гнилой помидор — «юнит-тесты не должны быть завязаны на внешние ресурсы!». Согласен, но как протестировать HTTP-клиент не делая HTTP-запроса? Варианты из стабом всего сетевого стека или запуском локального сервера для тестов — слишком затратные.

Итак, запускаем код и видим что тест не проходит (точнее, он даже не компилируется). Исправляем это добавлением в проект RSSReader нового модуля HttpClient, и подключаем этот модуль к нашему тест-кейсу. В модуле HttpClient создаем функцию с уже определенным интерфейсом. Здесь очень важный момент, мы пишем функцию ПОСЛЕ написания теста, т.е. тест диктует нам то, какой она должна быть (test driven development).

function GetPageByURL(URL: string): string;
begin

end;

Запускаем тест-кейс, тест не проходит. Теперь можно смело гуглить на тему «как получить содержимое страници по URL в Delphi» 🙂 Я выбираю Indy, т.к. он идет в поставку вместе с Delphi, но если у вас на проекте используется Synapse или другая библиотека — можно использовать ее. Вот моя реализация:

function GetPageByURL(URL: string): string;
var
  HTTP: TIdHTTP;
  Stream: TStringStream;
begin
  HTTP := TIdHTTP.Create;
  Stream := TStringStream.Create;
  try
    HTTP.Get(URL, Stream);
    Result := Stream.DataString;
  finally
    FreeAndNil(HTTP);
    FreeAndNil(Stream);
  end;
end;

Тест проходит, можно считать первую итерацию законченной и делать коммит 🙂 a655353f5e

ИТЕРАЦИЯ №2

На этой итерации займемся парсингом XML. Следуя философии TDD сперва создадим тест (и новый тест-кейс — TestRSSParser), причем максимально простой.

procedure TRSSParserTest.ItParsesRSSFeed;
var
  FeedContent: string;
begin
  FeedContent := '';  // TODO: Load dummy feed content
  ParseRSSFeed(FeedContent, ???);
end;

И снова TDD заставляет нас подумать об интерфейсе функции перед ее реализацией. С аргументом все вполне понятно — это будет строка с XML (это удобней всего в нашем случае). А что возвращать? Посмотрев внутреннюю структуру RSS и проанализировав, что в ней интересует нас больше всего, я пришел к выводу что нам нужен класс, который будет представлять записи RSS-ленты в Delphi, т.е. некая модель.

На этом я считаю итерацию закрытой, хотя ее видимых результатов нет (как жаль что DUnit не позволяет обозначить тест как pending / ожидающий). Зато у нас расширилось представление о внутренностях нашего будущего приложения. 76926b2e18

ИТЕРАЦИЯ №3

Разработку модели начинаем с чего? Правильно, с теста!

procedure TRSSFeedModelTest.ItHasDescription;
var
  Feed: TRSSFeed;
begin
  Feed := TRSSFeed.Create;
  Feed.Description := 'Some feed';
  CheckEquals('Some feed', Feed.Description);
  FreeAndNil(Feed);
end;

Естественно, код не скомпилируется. Добавим к проекту реализацию класса TRSSFeed в модуль RssModel.

type
  TRSSFeed = class
  private
    FDescription: string;
  public
    property Description: string read FDescription write FDescription;
  end;

Аналогично первому, добавляем тесты ItHasLink и ItHasTitle. Каждый из них будет создавать новый экземпляр TRSSFeed и проверять наличие соответствующего свойства. После того, как все тесты станут «зелеными», проведем рефакторинг кода тестов. В тест-кейсе переопределим методы SetUp и TearDown. Первый будет вызван перед каждым тестом, последний — после. Очень удобно создать поле для класса TRSSFeedModelTest.FFeed: TRSSFeed и создавать/разрушать экземпляр TRSSFeed в SetUp/TearDown. Тесты будут выглядеть таким образом:

procedure TRSSFeedModelTest.ItHasDescription;
begin
  FFeed.Description := 'Some feed';
  CheckEquals('Some feed', FFeed.Description);
end;

Аналогично создаем остальные свойства модели. Конечный результат можно посмотреть в коммите: 8bc87aae24

ИТЕРАЦИЯ №4

Возвращаемся к тестам нашего парсера. Я остановился на следующей реализации теста:

procedure TRSSParserTest.ItParsesRSSFeed;
var
  FeedContent: string;
  RSSFeed: TRSSFeed;
begin
  FeedContent := IOUtils.TFile.ReadAllText('feed.xml', TEncoding.UTF8);
  RSSFeed := ParseRSSFeed(FeedContent);
  CheckEquals('Delphi Zen', RSSFeed.Title);
  // TODO: Add more checks here
end;

Я оставляю право создать экземпляр TRSSFeed функции ParseRSSFeed, т.к. в дальнейшем нам может понадобится поддержка Atom и других подвидов RSS, в результате чего наша модель данных сможет разветвится на несколько классов. У ParseRSSFeed в таком случае будет больше свободы (она сама сможет выбрать экземпляр какого из классов вернуть).

Теперь разберемся с тестовыми данными. Сохранять XML как строку или запрашивать XML при каждом вызове теста — не самая хорошая идея, поэтому мы RSS-ленту положим в файл feed.xml рядом с exe-шником тестового проекта (feed.xml надо будет добавить в систему контроля версий).

Дело за малым — распарсить XML, создать и заполнить объект класса TRSSFeed. Сначала создаем новый модуль RssParser, подключаем к проекту, добавляем в него пустую функцию ParseRSSFeed, которая возвращает nil. Запускаем тест и видим что он провалился с AV. Ок, попробуем вернуть TRSSFeed.Create. Теперь вместо AV получаем ошибку тестирования — RSSFeed.Title должен содержать строку ‘Delphi Zen’. Займемся кодом, который сделает то, что от него требуется:

uses
  XMLDoc, XMLIntf;

function ParseRSSFeed(XML: string): TRSSFeed;
var
  Doc: IXMLDocument;
begin
  Doc := LoadXMLData(XML);
  Result := TRSSFeed.Create;
  Result.Title := Doc.DocumentElement.ChildNodes.First.ChildNodes.FindNode('title').Text;
end;

Тест проходит, но надо бы его расширить, ведь нас будут интересовать все поля TRSSFeed и его Item-ы. Попробуем так:

...
  CheckEquals('Delphi Zen', RSSFeed.Title);
  CheckEquals('http://delphi.frantic.im', RSSFeed.Link);
  CheckEquals('Food for thoughts', RSSFeed.Description);
  CheckEquals(9, RSSFeed.Items.Count);
...

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

Мне пришлось немного повозиться с реализацией парсера из-за нескольких глупых ошибок и некоторых особенностей RSS+TXMLDocument. Но очень приятно что отдача от тестов — очень шустрая, уже через 1 секунду после изменения в коде я вижу на что она повлияла.

Если в будущем наш проект столкнется с тем, что какой-то валидный XML он не сможет распарсить, очень рационально будет сохранить его в файл и добавить новый тест в TestRSSParser тест-кейс. Т.о. можно будет посредством тестирования определить, что пошло не так и исправить ошибку в тестовом проекте. После исправления следует оставить новый тест, возможно в будущем мы внесем изменения в парсер таким образом, что именно этот тип XML снова «поломается».

По ходу реализации парсера возникает задача перевода строки формата «Mon, 06 Sep 2009 16:45:00 +0000» в дату. Сделаем это с помощью TDD: напишем сначала тест а потом реализацию.

procedure TRSSParserTest.ItParsesRSSDate;
var
  d: TDateTime;
begin
  d := ParseRSSDate('Mon, 06 Sep 2009 16:45:00 +0000');
  CheckEquals('2009-09-06 16:45:00', FormatDateTime('yyyy-mm-dd hh:nn:ss', d));
end;

Штатных средств проверки дат на равенство в DUnit, к сожалению, нет. Если сравнивать 2 даты через CheckEquals, DUnit воспримет их как 2 значения с типом double, и в случае ошибки понять чем же эти даты отличаются будет трудно. Реализацию функции ParseRSSDate можно найти в исходниках этой итерации f2755e7927.

ИТЕРАЦИЯ №5

Просмотрев тесты к предыдущим итерациям, я заметил что совершенно выпустил из виду обработку ошибок. В TDD вопрос «Как будет вести себя код, если сервер недоступен?» совершенно неверный. Корректный вариант: «Как этот код ДОЛЖЕН себя вести, если сервер недоступен?». Я хочу, что бы GetPageByURL генерировал Exception если что-то пошло не так:

procedure THttpClientTest.ItRaisesAnExceptionWhenServerIsDown;
begin
  CheckException(RequestDeadServer, EHttpClientException);
end;

procedure THttpClientTest.RequestDeadServer;
begin
  GetPageByURL('http://127.0.0.1:9919/');
end;

Добавляем описание EHttpClientException в модуль HttpClient, запускаем тест и видим что он не проходит, т.к. вместо EHttpClientException получили EIdSocketError. EIdSocketError нас не устраивает, т.к. он завязан на Indy, а значит если мы захотим написать обработчик этого exception’а, нам придется явно подключать модули Indy в код. Заменить Indy на что-то другое или обновить версию Indy в дальнейшем будет очень трудно. Аналогично создаем тест ItRaisesAnExceptionWhenServerReturnsError.

Код итерации 7928c3676b.

ИТЕРАЦИЯ №6

Теперь возьмемся за парсер RSS. Аналогично HTTP-клиенту, я хочу в случае ошибки получить Exception. Стратегия создания теста и кода подобна HttpClient, поэтому я только привожу результат итерации: ff7d418026.

ИТЕРАЦИЯ №7

А вот теперь начнется самое интересное! Я возьмусь за написание основной бизнес-логики, которая зависит от HTTP-клиента, RSS-парсера и остальных сервисов, которых пока нет. Модуль SyncManager будет заниматься скачиванием RSS-ленты, парсингом, сохранением в хранилище и отображением визуальных оповещений. Но нам надо протестировать класс, а для этого нужно иметь возможность заменить реализацию его компонентов на заглушки. Сделаем это максимально просто, никаких Dependency Injection фреймворков и XML нам не понадобится.

Покажу как это можно сделать на примере с модулем HttpClient. Нужно разбить его на интерфейс и реализацию. Перепишем модуль HttpClient следующим образом. Интерфейс (модуль HttpClient):

type
  EHttpClientException = class(Exception);

  IHttpClient = interface
    ['{CDE443FE-8249-4557-8F26-D0EC6432F274}']
    function GetPageByURL(URL: string): string;
  end;

var
  DefaultHttpClient: IHttpClient;

Реализация (модуль IndyHttpClient):

type
  TIndyHttpClient = class(TInterfacedObject, IHttpClient)
  public
    function GetPageByURL(URL: string): string;
  end;

  ...

function TIndyHttpClient.GetPageByURL(URL: string): string;

...

Соответственно нам придется немного подкорректировать тест-кейс (заменить GetPageByURL на DefaultHttpClient.GetPageByURL, написать SetUp/TearDown).

Теперь о том, как это работает. Мы отделили интерфейс модуля от его реализации. Для того, что бы использовать HttpClient в нашей программе (или тесте) надо его инициализировать, например так: DefaultHttpClient := TIndyHttpClient.Create. Эту инициализацию логично вынести в функцию, которая будет вызвана во время старта приложения (в случае с тестом — в SetUp). Т.о. весь код, который требует для работы HTTP-функционал, будет зависеть ТОЛЬКО от модуля HttpClient, а реализацию можно будет заменить в любое время позже. В тесте, например, можно будет использовать какой-нибудь TFakeHttpClient и спокойно тестировать код, который зависит от HttpClient.

Этот подход очень удобен и спасал мои проекты не раз. Особенность в том, что он очень прост — нам не надо никаких внешних библиотек или сложных XML конфигураций, код легко понять и использовать (ведь в нем практически ничего нет!).

Еще следует отметить, что изначально я не начинал проектировать модуль HttpClient таким образом. Очень важно идти от простого к сложному, иначе можно построить слишком усложненный код, который будет только мешать. Возможно, в будущем, нам понадобится Dependency Injection или еще что-то в этом роде, но на данном этапе (и на многих следующих) нет смысла усложнять код.

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

Подобным образом я изменил RSS-парсер, с результатами можно ознакомится в коммитах ae34faadf7 и ecc5794acd. Весь этот процесс занял у меня не более 10 минут, но на практике чтобы отделить интерфейс от реализации нужно гораздо больше времени.

ИТЕРАЦИЯ №8

Я не начинал работу над подсистемой хранилища потому что пока слабо представляю какой именно функционал будет ему необходим. В этой ситуации надо подробней изучить техническое задание на приложение, поговорить с заказчиком, продумать варианты использования и т.д. В нашем упрощенном примере, я постараюсь определить требования к хранилищу работая над SyncManager’ом. Начинаем, как всегда, с теста:

procedure TSyncManagerTest.ItGetsAndStoresNewItemsWithNotification;
begin
  Sync('http://delphi.frantic.im/feed/');
end;

В основном приложении URL будет взят с конфигурационного файла или TEdit’а. Пустая процедура Sync уже делает тест «зеленым». Самое время подумать о том, как проверить что она делает именно то что мы хотим. Sync должен запросить ресурс по HTTP, распарсить полученный RSS, сохранить его Item’ы в хранилище и показать UI-оповещение. Создадим интерфейсы недостающих служб 5bad892ac0:

type
  IRSSStorage = interface
    ['{7F674ACD-6322-4419-BCCB-D8788E75ED53}']
    procedure StoreItem(Item: TRSSItem);
  end;

var
  DefaultRSSStorage: IRSSStorage = nil;

Визуальные оповещения будут использоваться как для новых RSS-записей, так и для сообщений об ошибках:

type
  IUINotificationService = interface
    ['{A8E7E1F7-E60F-465A-9D8C-0B6186CA7A06}']
    procedure Notify(Item: TRSSItem);
    procedure NotifyError(E: Exception);
  end;

var
  DefaultUINotification: IUINotificationService = nil;

Но и этого еще недостаточно. Для запуска тестов нам надо написать заглушки для служб. Новый Delphi RTTI в комбинации со специализироваными фреймворками позволяют это делать на ходу, но мы не будем усложнять пример и сделаем все вручную. Результат можно посмотреть здесь: 3aed5be638. Принцип простой — для каждого интерфейса я создаю реализацию, которая будет вызывать заданную ей анонимную процедуру (referance to procedure/function).

Теперь можем приступить к самому важному тесту (точнее, к его первому приближению):

procedure TSyncManagerTest.ItGetsAndStoresNewItemsWithNotification;
var
  ParserWasCalled: Boolean;
begin
  // HttpClient will return some dummy XML
  FFakeHttpClient.OnGetPageByURL :=
    function (URL: string): string
    begin
      CheckEquals('http://delphi.frantic.im/feed/', URL);
      Result := 'SomeXML';
    end;

  // Parser should be called with that dummy XML
  FFakeRSSParser.OnParseRSSFeed :=
    function (XML: string): TRSSFeed
    begin
      ParserWasCalled := True;
      CheckEquals('SomeXML', XML);
    end;

  Sync('http://delphi.frantic.im/feed/');
  CheckTrue(ParserWasCalled, 'Parser should have been called');
end;

Delphi — не динамический ЯП, поэтому такие штуки, как создание и проверка моков и стабов довольно затратная (по количеству кода и сложности). Но в принципе можно понять что делает этот тест. Заглушка HTTP-клиента вернет некоторую строку, а заглушка парсера проверит, что она вызвана именно с этой строкой. Переменная ParserWasCalled нужна для того, что бы в конце теста проверить, был ли вызван парсер. Запускаем тест, получаем ошибку «Parser should have been called». Теперь самое время приняться за реализацию:

procedure Sync(URL: string);
var
  Xml: string;
begin
  Xml := DefaultHttpClient.GetPageByURL(URL);
  DefaultRSSParser.ParseRSSFeed(Xml);
end;

В TDD очень часто случается так, что код, который тестируется по объему намного меньше самого теста. И это нормально. Продолжим развивать наш тест:

...
  // Storage should receive both items from RSS
  StoredItemsCount := 0;
  FFakeRSSStorage.OnStoreItem :=
    procedure (Item: TRSSItem)
    begin
      Inc(StoredItemsCount);
    end;

  Sync('http://delphi.frantic.im/feed/');
  CheckEquals(2, StoredItemsCount, 'StoreItem should have been called 2 times');
...

Т.е. теперь мы ожидаем, что Sync(…) в конце сохранит полученные записи (которые мы создаем заранее в SetUp) Cм. полные исходники к этой итерации https://github.com/frantic/delphi-tdd-example/commit/94b92ee7915d9ba34678cd17d2377920600f0d3b.

А ЧТО ДАЛЬШЕ?

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

Пример можно расширять, вводя новые службы — настройки и логирование (разумеется, вместе с тестами для них). В тестовом окружении настройки могут быть жестко заданы в коде, а в конечном приложении — в Ini/Xml/Json/YAML-файле или реестре. Разделяя интерфейс и реализацию вы значительно расширяете свои возможности и гибкость.

Больше того, если Вам понадобится перенести подобное приложение на MacOS или Linux, основная часть кода останется без изменений. Придется только заменить РЕАЛИЗАЦИИ некоторых модулей такими, что их сможет скомпилировать FreePascal на соответствующей платформе (например заменить Indy на Synapse).

Как быть со старым кодом?

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

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

Еще одной способ — начинать писать тесты только для нового кода. Если необходимо, что бы новый код взаимодействовал со старым, выделите это взаимодействие в отдельный интерфейс.

Но есть проблемы, не весьма инженерные. Если Вы работаете в команде над проектом заказчика, последний с 99% вероятностью будет против введения юнит-тестирования, т.к. это не дает очевидных видимых для него улучшений. Можно попробовать объяснить, но практика показывает что лучше начинать изменения изнутри, маленькими шагами. Попробуйте внедрить тесты в проект, после нескольких месяцев вы сможете понять насколько проще стал процесс разработки.

Бывает и так, что в команде разработчиков к идее автоматического тестирования относятся весьма пессимистично. В таком случае Вы можете попробовать этот подход сами в отдельной ветке (Вы же используете систему контроля версий?) и потом, если эксперимент удастся, показать результаты совей команде. Очень вероятно, что на живом примере ваши коллеги изменят свою точку зрения.

И последняя проблема: воодушевившись преимуществами юнит-тестирования, человек с головой погружается в них на несколько дней а потом сталкивается с проблемами, теряет интерес и забивает. Это, пожалуй, самая большая проблема. Начинайте с малого, постепенно вводите тесты в проект, пусть написание тестов станет полезной привычкой.

Советы

Напомню, что советы — не догма, но во многих случаях они весьма полезны.

Советы из личного опыта:

  1. Делайте тесты «плоскими», без if, while и т.д. В тесте не должно быть логики.
  2. Запускайте тесты как можно чаще. Слишком медленные можно отключить в DUnit (если то, над чем Вы работаете, на них не влияет).
  3. Начинайте с простого. Да, в интернете есть много хитрых инструментов для тестирования, не повышайте сложность без необходимости.
  4. Если используете Continuous Integration, включите запуск консольного варианта тестового проекта в скрипт сборки.
  5. Тестирование не сделает Ваш проект успешным и не избавит от 100% багов. Но очень в этом поможет.

Советы от Testivus (вольный перевод)

  1. Если пишете код, пишите тесты
  2. Не воспринимайте тестирование как догму
  3. Воспринимайте тестирование как карму
  4. Думайте о коде и тесте как об одном целом
  5. Тестирование важней чем ограничивающие правила
  6. Лучше всего писать тесты когда код еще горячий
  7. Тесты, которые не запускаются систематично, пустая трата времени
  8. Неидеальный тест сегодня лучше, чем идеальный тест когда-то в будущем
  9. Некрасивый тест лучше чем вообще без теста
  10. Хорошие тесты проваливаются

FEEDBACK

Мне очень важно Ваше мнение. Вопросы, замечания и пожелания оставляйте в комментариях. Если информация была полезна для Вас — поделитесь с товарищами по цеху 🙂 Спасибо за чтение!

Следующая запись
Оставьте комментарий

40 комментариев

  1. Спасибо за статью. Наконец-то хоть кто то взялся за раскрытие тестов. Ждем продолжения.

    Ответить
    • Frantic

       /  1 марта, 2012

      Что именно Вас интересуе больше всего?

      Ответить
      • Интересует тестирование уже готового проекта, который ранее разрабатывался бех использования тестов. Так сказать идти от обратного.

      • Frantic

         /  1 марта, 2012

        Если у Вас есть небольшой готовый проект, которым можете поделиться, попробую 🙂

      • В таком случае тесты нужно писать при рефакторинге (для куска, который рефакторите, и его ближайшего окружения и для нового кода), чтобы быть уверенным в каждом следующем шаге. А пытаться покрыть legacy code тестами — это тяжелый неблагодарный код.

  2. Bonart

     /  1 марта, 2012

    1. Приведенное определение юнит-теста вводит в заблуждение. Главное свойство такого теста — проверять тестируемую программную единицу изолированно. Без этого получатся страшные тесты, выполняющиеся часами и падающие не от ошибок в коде, а от мелких косяков в настройке тестовой среды.
    2. На вопрос когда надо писать, а когда можно не писать тесты ответ дан неконкретно, хотя есть очень хороший критерий: юнит-тест обязан быть качественно проще того, что тестируется и выполняться быстро. По такому критерию нет смысла покрывать юнит-тестами очень часто меняющиеся интерфейсы (издержки на актуализацию тестов), примитив (вероятность посадить баг только возрастет за свой же счет), ввод-вывод (работает медленно, сильно зависит от настроек среды).
    3. Хорошо покрываются тестами простые интерфейсы со сложной логикой реализации. Сответсвенно, если есть желание получить от тестов профит, логику надо железной рукой физически отделять от максимально тонкого ввода-вывода, взаимодействуя через простые интерфейсы — тогда юнит-тестами можно будет покрыть максимальное количество кода с минимальными усилиями.
    4. Надо очень четко понимать, что дают юнит-тесты — быстрое обнаружение фиксированного множества ошибок, не более и не менее. От кривой архитетуры тесты не спасут.

    Ответить
    • Frantic

       /  1 марта, 2012

      Дельные замечания. А что Вы думаете о TDD как технологии разработки?

      Если у Вас есть большой опыт в этой сфере, почему бы не поделится с сообществом?

      Ответить
    • Я бы отделил unit testing от TDD. Помимо unit, бывают и другие тесты — функциональные и тп.

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

      Например, можно сделать тесты для реализации use case (функциональные), тесты для проверки фиксенных багов из багтрекера (расширяют unit тесты), и юнит тесты к core — модулям (собственно — unit test). И применять тестирование в сочетании с mock/stub объектами.

      Тогда тестирование будет полезно не только в приведенных вами случаях!)

      Ответить
  3. runningmaster

     /  1 марта, 2012

    Это первая статья по искомой тематике, которая вызывает внутри меня ощущение собственной неполноценности при мысли о том, что я давно знаю про DUnit, но до сих не придавал этому серьезного значения в своей практике.

    Автору огромное спасибо за то, что взял на себя труд преподнести суть вопроса согласно поговорке о том, что лучше один раз увидеть. чем сто раз услышать (с)

    Еще один момент, на который стоит обратить внимание, ИМХО. В этой одной статье на самом деле две статьи. Одна выдвинута явно на передний план (TDD), а вторая идет фоном между строк и показывает как вообще следует разрабатывать код. Отчетливо прослеживается понимание сути подхода к снаряду (с) Поддерживаю.

    Ответить
  4. kylt_lichnosti

     /  1 марта, 2012

    Хоть убейте — не понимаю — как писать тест раньше структуры модуля. Ну надо же, хотя бы на бумажке, накидать интерфейс.
    Возможно вопрос привычки.

    Ответить
    • Frantic

       /  1 марта, 2012

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

      Ответить
      • kylt_lichnosti

         /  2 марта, 2012

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

        Удивлен, что так мало комментариев. Наверное все бросились писать тесты.

        А интерфейс у этой программы будет без дфм-ов? 🙂

      • runningmaster

         /  3 марта, 2012

        Не надо выбрасывать ничего. Хотя… есть же шаблон не использовать шаблоны? 🙂

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

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

        Про dfm. Не всегда есть в этом смысл, да. Со временем можно получить просветление, сопоставимое с таковым в дзене, что dfm и иже с ним есть то, без чего легче существовать в суе большому и толстому проекту. Обратите внимание на мою политкорректную формулировку. Я не сказал сразу, что это «плохо» или «хорошо». Я как бы намекнул, исходя из практического опыта, что буду предан анафеме за такое инакомысление в сообществе Delphi разработчиков. Да, можно разрабатывать корпоративные проекты без единой dfm (да это просто зависимость дополнительная)! Клацать мышкой всегда будет дольше, чем написать код в редакторе (при правильном подходе к проблеме!).

        Представьте, что вы можете развивать проект ни разу не открывая среду разработки! Хотя не рекомендую это делать, так как текстовый редактор в Delphi просто замечательный. Ну и все на этом. Ой, да там все остальное тоже крутое, только им можно всем с успехом не пользоваться 🙂

        Хорошо это или плохо? Зависит от ситуации. Лично я предпочитаю не получать шизофрению в логике приложения, когда код неявно размазывается между dfm и pas, а также проблемы с мержами в dfm. Каждый выбирает то, что ему лучше согласно текущей ситуации.

      • Frantic

         /  3 марта, 2012

        TDD — это прагматический подход к разработке кода, который не исключает шаблоны проектирования. Он помогает понять где нужна абстрактная фабрика, а где достаточно простого конструктора.

        ИМХО лучше идти по более прагматическому пути, чем в начале строить абстракции, которые только запутывают структуру приложения.

    • Наиль

       /  20 марта, 2012

      to kylt_lichnosti
      > Хоть убейте – не понимаю – как писать тест раньше структуры модуля
      Структуру можно писать до тестов. А вот реализацию модуля, после создания тестов.

      Ответить
  5. Замечательная статья!
    Огромнейшее спасибо. Буду использовать её чтобы сеять «разумное-доброе-вечное» дело TDD среди знакомых делфистов. =)

    Ответить
  6. SomeKindOfMonster

     /  7 марта, 2012

    FreeAndNil, да еще в блоке try..finally … Какой ужас. Читайте хоть Ника Ходжеса: http://www.nickhodges.com/post/Using-FreeAndNil.aspx

    Ответить
    • Frantic

       /  7 марта, 2012

      Привычка с предыдущего места работы — там это было в корпоративном стандарте по форматированию кода.

      А вообще хотелось бы услышать feedback по статье вцелом и TDD, ведь неиспользование FreeAndNil не помогает писать и тестировать код… 🙂

      Ответить
      • SomeKindOfMonster

         /  25 марта, 2012

        1. К TDD я отношусь скептически. Тесты — вещь несомненно полезная и показанная к применению. Но делать код ради тестов — это все равно что делать автомобили ради креш-тестов, имхо конечно.
        2. Статья очень растянута. Чесно говоря — с трудом дочитал до 3 или 4 теста. Стало скучно и неинтересно. Может быть стоило разделить на описание тестирования и TDD отдельно, может использовать поменьше не относящихся к теме рассуждений. Думаю, лучше сконцентрироваться на втором и больше внимания посвящать основной теме.

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

      Я не очень знаком с деятельностью данного товарища, то уже фраза «Setting a pointer to nil doesn’t get you anything» наводит на мысли об э.. образованности написавшего. Очевидно, он не понимает той простой вещи, что зануление указателя гарантированно вызовет AV в случае повторного (после освобождения) памяти. А при вызове Free такой вызов может прокатить. А может и нет. Что чревато внезапными и неотлавливаемыми AV в конечном приложении.

      Так что моё мнение таково — используйте FreeAndNil в совокупности с if Assigned(…) then …

      Что же касается использования внутри try..finally — лучше перестраховаться. FreeAndNil не спасет от ошибок доступа и не исправит их, но позволит оперативно обнаружить.

      Для сравнения с Ником — http://www.gunsmoker.ru/2009/04/freeandnil-free.html

      Мои извинения за оффтоп, но негоже такие вещи приводить без противовеса. Новички ведь могут и поверить.

      Ответить
      • Уточню по поводу «образованности» — я имел в виду педагогическую сторону. Новичкам не имеет смысл лезть под капот языка. Пусть ставят везде FreeANdNil. Когда дело дойдет до интерфейсов и COM (где FreeANdNil порой мешает) они сами поймут, когда от этой практики можно отказаться.

      • SomeKindOfMonster

         /  25 марта, 2012

        Мда, сравнили человека, работавшего не один год на CodeGear/Embarcadero и имевшего непосредственное отношение к созданию Delphi (это я про Ника Ходжеса) и кодера EurekaLog, приводящего аргументы из разряда «вот мы наняли тупых Delphi-кодеров и чтобы избежать ошибок будем ставить FreeAndNil везде». А еще он предлагает вместо тайпкаста Sender: TObject к конкретному объекту использовать absolute… Хвалит goto. Каково, а?

      • А вы просто обращайте внимание на целевую аудиторию. И будет всё понятно.

        «… чтобы избежать ошибок будем ставить FreeAndNil везде…”. Ну, если вы такой крутой Гуру, что ошибок не делаете… И если ваш код потом попадет в руки к таким же, как вы, Гуру, которые тоже никогда ошибок не делают… ))

        Мне лично два десятка лет опыта не позволяют делать таких заявлений.

        А вообще, Я считаю что качество кода гораздо важнее возможности чувствовать себя «специалистом» не нуждающимся в страховочных ремнях. «Я настолько круто вожу машину, что никогда не попаду в ДТП и потому мне незачем пристегиваться. » — не странное ли рассуждение? Я полагаю — это рассуждение новичка, который только-только получил права и полгода откатался самостоятельно. Почувствовал, так сказать, свою крутость)

        Можно же вообще код не тестировать. Или тестировать только на своём домашнем ПК под правами админа. А потом появляются… не будем пальцем показывать.

        Ещё раз повторюсь: ставить везде FreeAndNil вместо Free — отличная практика для новичков. Со временем, набрав опыта они сами поймут в каких местах от неё можно отказаться.

        Если вы переросли уровень новичка, и знаете где ставить Free а где FreeAndNil — в чём проблема-то? Ставьте. А то от вашего первого поста, извините, юношеским максимализмом веет…

        А что касается Ника — спорный, очень спорный пост, учитывая его место работы. Столь категоричные вещи нужно преподносить с осторожностью и с указанием контекста.

      • SomeKindOfMonster

         /  30 марта, 2012

        Если так рассуждать, то нужно вообще к Delphi не подходить, так как в этом языке хоть и меньше способов «выстрелить себе в ногу», чем в C++, но тоже немало. Надо переходить на языки с управляемым кодом, причем чем более язык высокоуровневый, тем лучше. Идеальный, наверно, будет Visual Basic for Applications. А еще лучше программированием вообще не заниматься. От этого баги бывают, вот!

      • Согласен, что отказ от использования FreeAndNil — спорная весч. Например, в слегка асинхронных программах. Или когда «убивается» объект, а в программе есть всякие обработчики сообщений, срабатывающие при «удалении» объектов..

        В общем случае — FreeandNil полезен при не совсем линейном вызове методов удаления различных объектов, он позоваляет гарантированно отметить уже удаленный объект.

  7. Alexey

     /  20 марта, 2012

    К вопросу об «тестирование уже готового проекта, очень рекомендую изучить книжку
    http://www.ozon.ru/context/detail/id/4311012/
    В ней этот вопрос как раз плотно рассматривается. Правда примеры на C++, но это не меняет сути 🙂

    Автору спс за статью.

    Ответить
  8. Итерация номер 7
    » заменить реализацию его компонентов на заглушки»
    для чего применять COM? ведь можно просто создать абстрактный класс для THttpClient, создать от него наследников TIndyHttpClient, TFakeHttpClient подставлять то реальный объект, то заглушку

    Ответить
    • COM здесь совершенно ни при чем 🙂 А интерфейсы удобнее абстрактных классов (не всегда конечно).

      Ответить
  9. Дмитрий

     /  30 июня, 2013

    Спасибо!
    Вчера прочел — сегодня внедрять начал. Уже один тест написал. Реально на код под другим углом начинаешь смотреть.
    Пока буду внедрять на текущем проекте. На новом попробую начать писать с тестов.

    Ответить
  10. TEger

     /  27 марта, 2014

    Идея тестов хороша (для повышения надежности кода, если этого требует ТЗ), но это работа тестера — тестировать, а кодера — кодировать. Тесты должны быть автоматизированы — главная задача (тестировщика). Если разработка контролируется тестировщиками (с учетом ТЗ) и сообщается кодерам-програмистам об ошибках интерфейса — подход хорош. Но обычно программист может поставить $IFDEF DEBUG Assert(..)/WriteMyLog(Error), если в чем не уверен и этого достаточно, вместо работы с TDD. Возможность ведения полного автоматического аудита/протокола работы программы (например на уровне запросов к БД), сбор ошибок, реально происходящих у клиента(в его среде) — куда важнее. Процент ошибок в UI обычно невелик в Delphi — больше в реализации бизнес-логики. Нельзя забывать, что призвание тестирования в обнаружении ошибок, а не в гарантировании их полного отсутствия.

    Ответить
  11. TEger

     /  27 марта, 2014

    If(1=0) then Assert(‘Вашу программу ломают в отладчике !’);

    Ответить
  12. TEger

     /  27 марта, 2014

    Если вам нужно регрессионное тестирование алгоритма — TDD для Вас. Если зависимости от вашего алгоритма/модуля в других местах нулевые, то TDD излишен..

    Ответить
  13. Александр

     /  5 мая, 2014

    Этот подход известен давно сишникам — сначала писать удобное использование некоторой функциональности (класс, функция…), а потом уже реализовывать ее.
    Почитал коменты и, к сожалению, как и везде, нашел бурю в стакане — рассуждение на счет FreeAndNil и статических ТЗ, даже немцы постоянно изменяют части системы в процессе разработки, да не один раз, а у них-то архитекторов и дизайнеров всегда достаточно на проектах — так что без тестов никуда, особенно теперь когда есть внутренний инструмент.
    Про отдельных тестеров — это несомненно лучше, но есть одно большое но. Буквально неделю назад, мне (в очередной раз) передали проект с отдельными тестами. Так вот:
    1) для тестирования использовалась теперь уже не поддерживаемая программа,
    2) тестеры тоже люди и любят халтурить, поэтому тесты были очень слабые, в смысле вариантов использования функции мало (чаще всего 1), что не соответствовало документации, но ведь никто не будет проверять каждую строчку.
    3) последнее тестирование было проведено автоматически сразу после сборки проекта (о чем говорят логи и даты) — и закончилось без ошибок. Но после того, как я все настроил, первый же прогон СТАРОГО исполняемого файла провалился. Причина — ошибки типов аргументов, типов возвращаемых значений, преобразования типов и тд. То, получаем, нерабочие тесты или не подходящий ехе файл (собранный мной тоже не проходит тест). Тестировщиков этих теперь нет, текущая команда загружена и запрос на разбор ситуации поставлен в план (~ 2 мес). Итого: что бы добавить пару форм и изменить 3 строчки кода — будет потрачена уйма времени, ведь нельзя понять правильно ли работает код или нет, надо все проверять, а это деньги, да и заказчик уже не хочет слышать о тестировании за которое заплатил при разработке и которое по итогу не работает.
    Вот так и это не единичный случай.
    Кстати с юнит-тестами намного проще в этом отношении. Ранее, когда не было встроенной системы, для тестирования просто писали отдельный класс, но тестировались не отдельные моменты, а изменения состояния сущности; например: создать разрешение, приостановить, продлить (закрыть старое, создать новое, активировать).

    Ответить
  14. Сергей Беловъ

     /  15 декабря, 2014

    Идею называть тесты «ItGetsContentByURL» я подсмотрел в мире Ruby.
    Для тех, кто не в мире Ruby, что значит It?

    Ответить
  15. Сергей Беловъ

     /  17 декабря, 2014

    Спасибо. Вот так всё просто. Мне думалось, что It это какое-то сокращение где t — test, а I — что-то еще.

    Ответить
  16. Михаил.

     /  25 декабря, 2017

    Доброго времени суток!!!
    Прочитав статью, появилось желание выпить, без закуски. Потому что проект написан большой, а ни одного теста к нему не написано…. Ну как говориться «Лучше поздно, чем никогда!». Будем пробовать.

    Ответить
  17. Destroyer86

     /  30 июня, 2021

    Привет, очень полезная статья, а про DI расскажешь?

    Ответить

Оставьте комментарий