Почему Вы должны сейчас все бросить и начать практиковать TDD

в 13:04, , рубрики: continuous testing, tdd, высокоуровневая архитектура, новый взгляд, метки: , , ,

С моим первым опытом TDD на меня снизошло озарение. Последние 2-3 года со всех сторон атакует информация «TDD это хорошо», «Тебе нужно TDD», отчего мозг инстиктивно начинает противиться насаждаемой информации. До этого я ни разу в коммерческих проектах не практиковал TDD, но когда начал — все встало на свои места. Хочу показать одну интересную на мой взгляд точку зрения на автоматизированные тесты, которой еще ни разу ни от кого не слышал.

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

Зачем Вам компилятор?

Компилировать? Я как-то раз понял, что не только. Точнее не для того, чтоб получить какие то бинарники, DLLки. Набирая код, я время от времени смотрю в окно с ошибками и исправляю если что-то появилось. Такую замечательную возможность мне предоставляет парсер, который крутится в фоне и проверяет код на наличие простейших ошибок. Но иногда этого недостаточно. Иногда компилятору надо скомпилировать/собрать все исходники проекта, чтоб обнаружить ошибку. Такая ситуация случается например в ASP.NET, когда из своего usercontrol'а мы удаляем публичный метод, но ошибки не видим, так как компилятор должен пересобрать этот юзерконтрол. В таких случаях нужно полностью пересобрать проект, но заметьте, что нам по сути не нужно то, что мы получили в результате сборки, нам на данном этапе была важна только информация об отсутствии/наличии ошибок.

К чему я клоню?

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

class Dart
{
	public void FeelForce()
	{
		...
	}
	
	public void  DoHeavyBreath()
	{
		...
	}
}

class Luke : Dart
{
	public void DefeatAllEnemies()
	{
		...
	}
}

void main()
{
	Luke luke = new Luke();
	luke.FeelForce();
	luke.DefeatAllEnemies();
	luke.DoHeavyBreath(); // нет, это не то, что может делать Люк
}

Вызов метода DoHeavyBreath у класса Luke — логическая ошибка не видимая компилятору. Это ошибка более высшего уровня. Но программирование с каждым годом стремится вверх, становится все абстрактнее, поэтому нам нужны инструменты, чтоб справляться со сложностью. Один из необходимых инструментов является парсер ошибок более высшего уровня. Так же как нам компилятор указывает на синтаксические ошибки.

Код выше стоило бы переписать так для большей грамотности:

abstract class Jedi 
{
	public void FeelForce()
	{
		...
	}
	
	public virtual void DefeatAllEnemies()
	{
		throw new NotImplementedException();
	}
	
	public virtual void DoHeavyBreath()
	{
		throw new NotImplementedException();
	}
}

class Dart : Jedi
{
	override void DoHeavyBreath()
	{
		... // do heavy breath
	}
}

class Luke: Jedi
{
	override void DefeatAllEnemies()
	{
		... // defeat all enemies
	}
}

void main()
{
	Luke luke = new Luke();
	luke.FeelForce();
	luke.DefeatAllEnemies();
	luke.DoHeavyBreath(); 
}

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

void test_that_luke_cannot_do_heavy_breath()
{
	try
	{
		main();
	}
	catch(NotImplementedException ex)
	{
		Assert.Fail("Unimplemented method was called!");
	}
}

Я лично вижу в таком подходе ничто иное как инструмент для борьбы со сложностью высокоуровневых архитектур. У мозга есть свой предел, который он может охватить, а нужные инструменты расширяют эти пределы. На высоких уровнях абстракции компилятор теряет свою роль «указателя на ошибки», так как основной процент ошибок он не в состоянии охватить. TDD в состоянии. Конечно же методика еще будет развиваться. В идеале разработчик должен знать все существующие баги (логические в том числе) до того, как он скомпилирует проект, ему помогут существующие и будущие инструменты, с которыми в основном ассоциируется TDD. Это развитие даже уже можно заметить. Кто интересуется могут почитать про NCrunch для Visual Studio. Думаю под другие IDE такие инструменты тоже имеются, которые относятся к continuous testing. В двух словах — этот инструмент все время в фоне запускает юнит тесты и Вы сразу видите результаты тестов по мере написания кода без перекомпиляции. Вы заметите некое сходство с тем, как компилятор в фоне ищет синтаксические ошибки и выдает по мере нахождения. А теперь же инструменты стали настолько умны, что показывают еще и логические ошибки. Конечно можно высказывать свое недовольство по поводу того, что тесты все равно приходится писать самому и можно упустить из виду какую то логическую ошибку, но я думаю это все еще впереди. Может быть когда-то тесты будут сами генерироваться из какого-то формального описания бизнес логики. В любом случае лучше радоваться тому, что есть, чем плакать по тому, чего нет.

Автор: amorphius


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


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