Лекция 07 - Модульное тестирование ПО.pptx
- Количество слайдов: 62
Модульное тестирование ПО
Почему Ваш код – отстой? Ваш код – отстой, если он не работает Вы уверены, что ваш код работает? Вы уверены, что после рефакторинга ваш код будет продолжать работать? Ваш код – отстой, если он не поддаётся тестированию Требуются изменения для проведения автоматизированных тестов? Вы уверены, что после этих изменений код будет продолжать работать?
Способы улучшения ситуации Ручное тестирование кода Автоматическое тестирование кода Модульное тестирование Интеграционное тестирование В любом случае: «Тестирование программ может использоваться для демонстрации наличия ошибок, но оно никогда не покажет их отсутствие. » - Дейкстра, 1970
Недостатки ручного тестирования ПО Глубокое ручное тестирование сложного ПО – очень трудоемкий и дорогостоящий процесс А вам легко протестировать 300 000 строк кода? Результаты тестов не сохраняются и их сложно повторить заново
Что такое Модульное тестирование? Модульное тестирование (Unit testing) – процесс проверки корректности отдельных модулей (классов) программы Основная идея - разработка тестов, проверяющих работу каждой нетривиальной функции или метода модуля Для облегчения разработки модульных тестов используются различные фреймворки
Разработка через тестирование (test-driven development, TDD) – техника программирования, при которой модульные тесты пишутся до модулей программы, тем самым управляя их разработкой Разработка состоит из коротких циклов (шагов), продолжительность которых обычно составляет несколько минут TDD – одна из практик Экстремального программирования
Этапы разработки в стиле Test Driven Development
Шаг 1. Извлечение кода из репозитория Из репозитория разработчик извлекает исходный код программной системы, находящейся в согласованном состоянии, когда весь набор существующих модульных тестов выполняется успешно. Этот шаг гарантирует, что разработчик имеет дело с последней рабочей версией исходного кода проекта
Шаг 2. Добавление теста К существующему набору тестов добавляется новый тест Этот тест может состоять в проверке, реализует ли система некоторое новое поведение или содержит ли некоторую ошибку, о которой недавно стало известно Важно написать тест до внесения изменений в код модуля, тем самым предъявляя к модулю новое требование
Пример #include “Serial. Number. Generator. h" // набор тестов для генератора серийных номеров class CSerial. Number. Generator. Test. Suite : public Cxx. Test: : Test. Suite { public: // ранее разработанные тесты //. . . // команда тестирования сообщила об ошибке: // если в качестве серийного номера передана пустая строка, то // серийный номер ошибочно принимается за правильный void Test. Empty. Serial. Number() { CSerial. Number. Generator sg; // серийный номер в виде пустой строки должен считаться невалидным TS_ASSERT(!sg. Verify. Serial. Number("User. Name", "")); } };
Шаг 3. Запуск теста Успешно выполняется весь набор тестов, кроме нового теста, который выполняется неуспешно Этот шаг необходим для проверки самого теста включен ли тест в общую систему тестирования (запускается ли)? правильно ли тест отражает новое требование к системе, которому она, естественно, еще не удовлетворяет? In CSerial. Number. Generator. Test. Suite: : Test. Empty. Serial. Number: . Serial. Number. Generator. Test. Suite. h(44): Error: Assertion failed: !sg. Verify. Serial. Number("User. Name", "") 1>Failed 1 of 4 tests
Шаг 4. Исправление ошибки с минимумом усилий Программа изменяется с тем, чтобы как можно скорее выполнялись все тесты. Нужно добавить самое простое решение, удовлетворяющее новому тесту, и одновременно с этим не испортить существующие тесты Большая часть нежелательных побочных и отдаленных эффектов от вносимых в программу изменений отслеживается именно на этом этапе, с помощью достаточно полного набора тестов
Пример class CSerial. Number. Generator { public: // серийный номер ошибочно принимается за правильный bool Verify. Serial. Number(string const& user. Name, string const& serial. Number) { if (serial. Numer. empty()) { return false; } //. . . } };
Шаг 5 – запуск тестов После внесения исправлений в код модуля весь набор тестов должен выполняться успешно Что, если это не так? Проверить корректность новых требований теста Ошибки или опечатки в тесте Совместимость с предыдущими требованиями Проверить корректность внесенных изменений в код модуля Исполнить «танец с бубном»
Шаг 6 - рефакторинг Когда требуемая в этом цикле функциональность достигнута самым простым способом, производится рефакторинг Процесс изменения внутренней структуры программы, не затрагивающий ее внешнего поведения и имеющий целью: облегчить понимание ее работы; устранить дублирование кода; облегчить внесение изменений в ближайшем будущем
Шаг 7 – запуск тестов Рефакторинг связан с внесением изменений, которые могут случайно нарушить работу кода Ошибки, вносимые при рефакторинге так же могут быть выявлены в результате автоматического тестирования Добиваемся успешного выполнения всего набора тестов на данном этапе
Шаг 8 – фиксирование изменений Комплект изменений, сделанных в этом цикле в тестах и программе заносится в репозиторий (операция commit) Теперь программа снова находится в согласованном состоянии и содержит четко осязаемое улучшение по сравнению с предыдущим состоянием
Разработка в стиле TDD Извлечение кода Добавление теста Фиксирование изменений Запуск тестов Внесение изменений Рефакторинг Запуск тестов
Пример Разработка класса, моделирующего самолет
Требования Реализовать класс Airplane, обладающий следующими свойствами Количество топлива Количество пассажиров на борту Наличие пилотов и стюардесс на борту Состояние (на земле, взлет, посадка) Количество посадочных мест Класс должен поддерживать следующие методы: Заправка топливом Подъем и спуск пассажиров Подъем и спуск членов экипажа Взлет Посадка
Шаг 1. Создание теста с использование библиотеки Cxx. Test class Airplane. Test. Suite : public Cxx. Test: : Test. Suite { public: void Test. Airplane. Construction() { Airplane airplane; } }; Данный код не будет компилироваться, т. к. отсутствует объявление класса Airplane Тем не менее, это позволяет нам убедиться, что наш тест был добавлен в проект
Создание каркаса класса Airplane и модификация теста class Airplane { public: Airplane() { } }; Убеждаемся, что набор тестов теперь проходит нормально #include “Airplane. h” class Airplane. Test. Suite : public Cxx. Test: : Test. Suite { public: void Test. Airplane. Construction() { Airplane airplane; } };
Шаг 2 – тестируем начальное состояние самолета Требования Для создания самолета необходимо указать количество мест в нем Количество пассажиров, пилотов, стюардесс и топлива в самолете после создания равно нулю Дорабатываем тест Test. Airplane. Construction, добавляя в него проверку начального состояния самолета
Добавление проверки нового функционала в тест class Airplane. Test. Suite : public Cxx. Test: : Test. Suite { public: void Test. Airplane. Construction() { // 100 - number of seats Airplane airplane(100); TS_ASSERT_EQUALS(airplane. Get. Fuel(), 0); TS_ASSERT_EQUALS(airplane. Get. Number. Of. Passengers(), 0); TS_ASSERT_EQUALS(airplane. Get. Number. Of. Seats(), 100); TS_ASSERT_EQUALS(airplane. Get. Number. Of. Pilots(), 0); TS_ASSERT_EQUALS(airplane. Get. Number. Of. Stewardess(), 0); // 50 - number of seats Airplane airplane 1(50); TS_ASSERT_EQUALS(airplane 1. Get. Number. Of. Seats(), 50); } };
Доработка класса Airplane для успешного прохождения тестов class Airplane { public: Airplane(size_t number. Of. Seats): m_number. Of. Seats(number. Of. Seats) { } double size_t private: size_t }; Get. Fuel()const{return 0; } Get. Number. Of. Seats()const{return m_number. Of. Seats; } Get. Number. Of. Passengers()const{return 0; } Get. Number. Of. Pilots()const{return 0; } Get. Number. Of. Stewardess()const{return 0; } m_number. Of. Seats; В глаза бросается тот факт, что многие Get-функции возвращают hardcodedзначения. На данном этапе – это минимальное решение, удовлетворяющее успешному прохождению тестов. В дальнейшем мы это исправим
Пришло время добавить возможность заправки самолета Выясняется, что самолет должен иметь еще одно дополнительное состояние – вместимость топливного бака Это новое требование мы должны также отразить в модульных тестах
Дорабатываем тест class Airplane. Test. Suite : public Cxx. Test: : Test. Suite { public: void Test. Airplane. Construction() { // 100 - number of seats, 500. 0 – fuel tank capacity Airplane airplane(100, 500. 0); TS_ASSERT_EQUALS(airplane. Get. Fuel(), 0); TS_ASSERT_EQUALS(airplane. Get. Number. Of. Passengers(), 0); TS_ASSERT_EQUALS(airplane. Get. Number. Of. Seats(), 100); TS_ASSERT_EQUALS(airplane. Get. Number. Of. Pilots(), 0); TS_ASSERT_EQUALS(airplane. Get. Number. Of. Stewardess(), 0); TS_ASSERT_EQUALS(airplane. Get. Fuel. Tank. Capacity(), 500. 0); // 50 - number of seats, 400. 3 – fuel tank capacity Airplane airplane 1(50, 400. 3); TS_ASSERT_EQUALS(airplane 1. Get. Number. Of. Seats(), 50); TS_ASSERT_EQUALS(airplane. Get. Fuel. Tank. Capacity(), 400. 3); } };
Дорабатываем класс Airplane class Airplane { public: Airplane(size_t number. Of. Seats, double fuel. Tank. Capacity) : m_number. Of. Seats(number. Of. Seats) { } double Get. Fuel()const{return 0; } double Get. Fuel. Tank. Capacity()const{return m_fuel. Tank. Capacity; } size_t private: size_t double }; Get. Number. Of. Seats()const{return m_number. Of. Seats; } Get. Number. Of. Passengers()const{return 0; } Get. Number. Of. Pilots()const{return 0; } Get. Number. Of. Stewardess()const{return 0; } m_number. Of. Seats; m_fuel. Tank. Capacity; Пристальный анализ исходного кода показывает, что мы забыли выполнить инициализацию поля m_fuel. Tank. Capacity в конструкторе класса
Запускаем тест Running 1 test In Airplane. Test. Suite: : Test. Airplane. Construction: d: vividunit_testingairplanetestsuite. h: 18: Error: Expec ted (airplane. Get. Fuel. Tank. Capacity() == 500. 0), found (-9. 2559 E 62 != 500. 0000) Failed 1 of 1 test Success rate: 0% Ошибка, допущенная нами при внесении изменений в класс была обнаружена на этапе модульного тестирования Поскольку изменения, вносимые в класс на каждой итерации, являются минимальными, это упрощает локализацию и исправление ошибки. В данном случае ошибка является тривиальной и легко исправляется
Исправляем класс Airplane class Airplane { public: Airplane(size_t number. Of. Seats, double fuel. Tank. Capacity) : m_number. Of. Seats(number. Of. Seats) , m_fuel. Tank. Capacity(fuel. Tank. Capacity) { } double Get. Fuel()const{return 0; } double Get. Fuel. Tank. Capacity()const{return m_fuel. Tank. Capacity; } size_t private: size_t double }; Get. Number. Of. Seats()const{return m_number. Of. Seats; } Get. Number. Of. Passengers()const{return 0; } Get. Number. Of. Pilots()const{return 0; } Get. Number. Of. Stewardess()const{return 0; } m_number. Of. Seats; m_fuel. Tank. Capacity;
class Airplane. Test. Suite : public Cxx. Test: : Test. Suite { public: void Test. Airplane. Construction() { // 100 - number of seats, 500. 0 – fuel tank capacity Airplane airplane(100, 500. 0); Running 1 test In Airplane. Test. Suite: : Test. Airplane. Construction: TS_ASSERT_EQUALS(airplane. Get. Fuel(), 0); d: vividunit_testingairplanetestsuite. h: 23: Error: Expec TS_ASSERT_EQUALS(airplane. Get. Number. Of. Passengers(), 0); ted (airplane. Get. Fuel. Tank. Capacity() == 400. 3), found (500. 0000 != 400. 3000) Failed 1 of 1 TS_ASSERT_EQUALS(airplane. Get. Number. Of. Seats(), 100); test Success rate: TS_ASSERT_EQUALS(airplane. Get. Number. Of. Pilots(), 0); 0% TS_ASSERT_EQUALS(airplane. Get. Number. Of. Stewardess(), 0); TS_ASSERT_EQUALS(airplane. Get. Fuel. Tank. Capacity(), 500. 0); Запускаем тесты // 50 - number of seats, 400. 3 – fuel tank capacity Airplane airplane 1(50, 400. 3); TS_ASSERT_EQUALS(airplane 1. Get. Number. Of. Seats(), 50); TS_ASSERT_EQUALS(airplane 1. Get. Fuel. Tank. Capacity(), 400. 3); TS_ASSERT_EQUALS(airplane. Get. Fuel. Tank. Capacity(), 400. 3); } }; Анализ кода показывает, что ошибка была допущена в самом тесте. Что ж, такое тоже бывает. Нужно просто внести исправление в сам тест.
Реализация поддержки заправки самолета При заправке самолета должно произойти увеличение находящегося в нем топлива на заданную величину Сначала пишем тест, проверяющий работу нового метода Refuel()
Добавляем новый тест, проверяющий возможность заправки самолета топливом class Airplane. Test. Suite : public Cxx. Test: : Test. Suite { public: void Test. Airplane. Construction() {. . . } void Test. Airplane. Refuel() { Airplane airplane(100, 500. 0); TS_ASSERT_EQUALS(airplane. Get. Fuel(), 0); airplane. Refuel(100. 0); TS_ASSERT_EQUALS(airplane. Get. Fuel(), 100. 0); airplane. Refuel(200. 0); TS_ASSERT_EQUALS(airplane. Get. Fuel(), 300. 0); } };
Доработка класса Airplane class Airplane { public: Airplane(size_t number. Of. Seats, double fuel. Tank. Capacity) : m_number. Of. Seats(number. Of. Seats) , m_fuel. Tank. Capacity(fuel. Tank. Capacity) , m_fuel(0) { } double Get. Fuel()const{return m_fuel; }. . . void Refuel(double amount. Of. Fuel) { m_fuel += amount. Of. Fuel; } private: size_t m_number. Of. Seats; double m_fuel. Tank. Capacity; double m_fuel; };
Дорабатываем тест – нельзя залить топлива больше, чем позволяет бак class Airplane. Test. Suite : public Cxx. Test: : Test. Suite { public: . . . void test. Airplane. Refuel() { Airplane airplane(100, 500. 0); TS_ASSERT_EQUALS(airplane. Get. Fuel(), 0); airplane. Refuel(100. 0); TS_ASSERT_EQUALS(airplane. Get. Fuel(), 100. 0); airplane. Refuel(200. 0); TS_ASSERT_EQUALS(airplane. Get. Fuel(), 300. 0); // amount of fuel can't exceed the airplane’s fuel tank capacity airplane. Refuel(300. 0); TS_ASSERT_EQUALS(airplane. Get. Fuel(), airplane. Get. Fuel. Tank. Capacity()); } };
Запускаем тест Running 2 tests. In Airplane. Test. Suite: : Test. Airplane. Refuel: D: vividunit_testingairplaneAirplane. Test. Suite. h: 38: Error: Expec ted (airplane. Get. Fuel() == airplane. Get. Fuel. Tank. Capacity()), found (600. 0000 != 5 00. 0000) Failed 1 of 2 tests Success rate: 50%
Дорабатываем класс Airplane class Airplane { public: . . . void Refuel(double amount. Of. Fuel) { m_fuel += amount. Of. Fuel; if (m_fuel > m_fuel. Tank. Capacity) { m_fuel = m_fuel. Tank. Capacity; } } private: size_t m_number. Of. Seats; double m_fuel. Tank. Capacity; double m_fuel; };
Проверка корректности аргументов Методы класса должны проверять корректность переданных аргументов Особенно это касается данных, поступающих извне (пользователь, сеть, входные устройства и т. д) Один из способов просигнализировать об ошибке – выбросить исключение Необходимо стремиться к тому, чтобы код был exception-safe При выбрасывании исключения внутри метода класса не должно происходить утечек ресурсов Не должна нарушаться целостность внутренней структуры объекта По возможности объект должен остаться в том же состоянии, в каком он находился до выброса исключения
Проверяем допустимые границы входных аргументов Не должно иметься возможности заправить самолет отрицательным количеством топлива Вместимость бака самолета должно быть в пределах от 300 до 5000 литров Количество посадочных мест – от 10 до 200
Доработка теста class Airplane. Test. Suite : public Cxx. Test: : Test. Suite { public: void Test. Airplane. Construction() {. . . for tests Running 2 (size_t seats = 0; seats <= 300; ++seats) { In Airplane. Test. Suite: : Test. Airplane. Construction: if ((seats >= 10) && (seats <= 200)) d: vividunit_testingairplanetestsuite. h: 33: Error: Expec { ted (Airplane(seats, 500)) to throw (std: : invalid_argument) but it didn't TS_ASSERT_THROWS_NOTHING(Airplane(seats, 500)); throw }. else Failed 1 of 2 tests { Success rate: 50% TS_ASSERT_THROWS(Airplane(seats, 500), std: : invalid_argument); } } }. . . };
Доработка класса Airplane class Airplane { enum { MIN_NUMBER_OF_SEATS = 10, MAX_NUMBER_OF_SEATS = 200, }; public: Airplane(size_t number. Of. Seats, double fuel. Tank. Capacity) : m_number. Of. Seats(number. Of. Seats) , m_fuel. Tank. Capacity(fuel. Tank. Capacity) , m_fuel(0) { if ((number. Of. Seats < MIN_NUMBER_OF_SEATS) || (number. Of. Seats > MAX_NUMBER_OF_SEATS) ) { throw std: : invalid_argument("Number of seats is out of range"); } }. . . };
Далее Аналогичным образом внедряются проверки и остальных входных аргументов
Преимущества использования Unit-тестирования Поощрение изменений Облегчение рефакторинга Легкость обнаружения ошибок Упрощение интеграции Устранение сомнений по поводу надежности отдельных модулей Документирование кода Тесты могут служить иллюстрацией использования класса Отделение интерфейса классов от реализации Позволяет уменьшить связность компонентов системы
Преимущества использования Unit-тестирования Тесты постоянно управляют процессом разработки модуля, предъявляя новые и новые формальные требования к системе Успешное прохождение автоматическим тестов свидетельствует о соответствии программы заданным требованиям Написанные однажды тесты служат верой и правдой на протяжении жизненного цикла программы На ручное тестирование приходится тратить силы постоянно Автоматические тесты выполняются гораздо быстрее ручных «Вкалывают роботы – счастлив человек» © Облегчается совместная работа нескольких человек над одними и теми же модулями
Мнимые недостатки автоматических тестов «Написание тестов увеличивает срок разработки» Еще больше его увеличивает поиск ошибок вручную Качественный код требует дополнительных временных затрат (примерно на 50% больше) «В процессе развития программы приходится тратить время на актуализацию ранее написанных тестов» Не на актуализацию тестов, а на актуализацию требований к программе «Для моих классов очень сложно написать модульные тесты» А как Вы умудрились их так криво написать?
Напутствие Тестирование Вашего кода - прежде всего, Ваша задача, а уж потом – тестировщика Не увлекайтесь чрезмерным написанием тестов Автоматическое тестирование и TDD – не панацея Эффективное их использование приходит с опытом
Как добавить модульные тесты к уже написанным классам? Разработка новых классов ведется в стиле TDD, а старый код пока не трогаем В отсутствие тестов повышается шанс незаметно сломать уже работающий код Изменение функционала или рефакторинг старых модулей по возможности сопровождается написанием тестов
Что должны покрывать модульные тесты? В идеале – всё На практике – критические и нетривиальные участки кода Код, подверженный частым изменениям Код, от которого зависит работоспособность большого количества другого кода Сложный код Код с большим количеством зависимостей
Проблемы, мешающие внедрению модульных тестов Побочные действия кода вывод в лог возникновение окон assertion’ов запись данных в файл вызов внешних библиотек Жесткие зависимости тестируемого класса Большое количество зависимостей Выполнение одним классом нескольких обязанностей
Устранение проблем (1) Вынесение кода, выполняющего побочные действия в отдельные классы Отключение записи в log и assertion-ов во время модульного тестирования Уменьшение зависимостей между модулями (добавление уровней косвенности) Замена конкретного интерфейса абстрактным Вынесение класса зависимости в параметры шаблона
Устранение проблем (2) Разбиение класса на более мелкие, каждый из которых несет ответственность только за одну функциональность Создание библиотек фальшивых объектов (Mock objects) для тестирования классов с большим количеством зависимостей
Пример Добавить возможность дозаправки самолета в воздухе, используя объект типа Tanker. Aircraft (самолетзаправщик) В настоящее время дозаправка в воздухе применяется только военными самолетами, но для нашей задачи – это не важно
Самолет и Заправщик class Airplane { public: . . . // заполняет бак самолета топливом у самолета заправщика double Refuel(Tanker. Aircraft & aircraft){. . . } }; class Tanker. Aircraft { public: . . . // запрашивает заданное количество топлива у заправщика // возвращает реально доступное, уменьшая количество топлива в заправщике double Request. Fuel(double amount){. . . } };
Проблема: связи между реализациями классов Тестирование класса Airplane затруднено из-за того, что он оказывается связанным с конкретным классом Tanker. Aircraft модульное тестирование предполагает возможность тестирования классов независимо друг от друга Для решения этой проблемы необходимо выделить интерфейс ITanker. Aircraft из класса Tanker. Aircraft и связать класс Airplane с выделенным интерфейсом
Решение: выделение интерфейса class ITanker. Aircraft { public: . . . virtual double Request. Fuel(double amount) = 0; }; class Airplane { public: . . . // заполняет бак самолета топливом у самолета заправщика double Refuel(ITanker. Aircraft & aircraft){. . . } }; class Tanker. Aircraft : public ITanker. Aircraft { public: . . . double Request. Fuel(double amount) {. . . } };
Фальшивый заправщик class Mock. Tanker. Aircraft : public ITanker. Aircraft { public: Mock. Tanker. Aircraft(double request. Result = 0) : request. Fuel. Result(0) , requested. Amount(0) { }. . . // заправщик-фальшивка возвращает заранее подготовленный результат, // запоминая последнее запрошенное количество топлива double Request. Fuel(double amount) { requested. Amount = amount; return request. Fuel. Result; } // значения данных переменных будут задаваться и проверяться в тесте double requested. Amount; double request. Fuel. Result; };
Использование фальшивого заправщика #include “Mock. Tanker. Aircraft. h“ class Airplane. Test. Suite : public Cxx. Test: : Test. Suite { public: . . . void Test. Refuel. Using. Tanker. Aircraft() { const size_t SEATS = 100; const double TANK_CAPACITY = 1000; // создаем самолет и фальшивый заправщик с 800 литрами топлива Airplane(SEATS, TANK_CAPACITY); Mock. Tanker. Aircraft tanker(800); // заправляем самолет у фальшивого заправщика plane. Refuel(tanker); // убеждаемся, что запросили топлива на полный бак TS_ASSERT_EQUALS(tanker. requested. Amount, TANK_CAPACITY); // убеждаемся, что реально увеличили топливо на столько, сколько дал заправщик TS_ASSERT_EQUALS(plane. Get. Fuel(), tanker. request. Fuel. Result); } };
Закон Деметра – «Разговаривай только с близкими друзьями» Любой метод M объекта O может вызывать лишь методы Объекта O Своих параметров Любых объектов, создаваемых внутри метода M Компонентов объекта O Полностью следовать этому закону затруднительно – взвешивайте все «за» и «против» Тем не менее, класс, удовлетворяющий данным требованиям, как правило, проще протестировать В нем меньше связей от других классов
Пример не очень хорошей архитектуры class Button {}; class Military. Button : public Button {}; class Finger { public: void Push(Button & button); }; class Human { public: Hand & Get. Right. Hand(); }; class Hand { public: Finger & Get. Thumb(); }; class President : public Human { }; void Start. The. War() { President the. President; Military. Button the. Red. Button; } the. President. Get. Right. Hand(). Get. Thumb(). Push(the. Red. Button);
Улучшаем архитектуру с использованием закона Деметра class Button {}; class Finger { public: void Push(Button & button); }; class Military. Button : public Button {}; class Human { public: Hand & Get. Right. Hand(); void Push(Button & button) { Finger & thumb = Get. Right. Hand(). Get. Thumb(); thumb. Push(button) } }; class President : public Human { }; class Hand { public: Finger & Get. Thumb(); }; void Start. The. War() { President the. President; Military. Button the. Red. Button; } the. President. Push(the. Red. Button);
Организационные аспекты Код, покрытый тестами имеет большую надежность и более высокое качество Разработка тестов требует дополнительного времени разработки Необходимо учитывать при оценке трудоемкости Разработчики должны поощряться за написание автоматических тестов и наказываться за их отсутствие Отсутствие автоматических тестов приводит к усложнению поддержи разработанного кода, усложнению ручного тестирования и, как следствие, к увеличению затрат на проект и сроков разработки
Ссылки Движение к автоматическому тестированию Ортогональность Несвязность и закон Деметра Разработка через тестирование Юнит-тестирование Рефакторинг Тестирование ПО Модульное тестирование Список фреймворков для модульного тестирования