Статьи

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

Разработка через тестирование, или TDD, часто указывается в качестве важной передовой практики Agile, и так оно и есть. Он творит чудеса в проектах «зеленых полей» и новых базах кода, где вы можете начать все заново и убедиться, что весь ваш код легко тестируем и хорошо протестирован. Но как насчет старого кода? (Под устаревшим кодом я имею в виду любой код, который не имеет исчерпывающего набора автоматических тестов, поэтому вы можете писать устаревший код, как мы говорим). Для большинства из нас большая часть кода, над которым мы когда-либо будем работать, изначально не была нашей собственной работой. И, к сожалению для индустрии программного обеспечения, только небольшая часть кода может действительно похвастаться всесторонними модульными и интеграционными тестами. Как такие методы, как разработка через тестирование, могут сделать нашу работу разработчиков более продуктивной и менее разочаровывающей?

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

На самом деле, разработка через тестирование (наряду с другими смежными методами, такими как разработка через поведение и разработка через приемочное тестирование) является ценным инструментом для работы с унаследованным кодом. Разработка через тестирование — это не просто гибкая практика; на самом деле это просто здравый смысл. Будете ли вы летать на самолете, в котором пилот только время от времени пробегал контрольный список перед полетом, или нет, и устранять какие-либо проблемы, возникающие в воздухе? Нет, не совсем. Автоматизированные модульные тесты похожи на контрольный список на плоскости — они позволяют быстро, автоматически и без особых усилий обнаружить проблемы. Использование тестовой разработки с существующим кодом похоже на поиск и устранение проблем с помощью контрольного списка пилота, а не взбирание на крыло и затягивание болта или двух.

Но использование Test-Driven Development для унаследованного кода немного отличается от использования TDD в контексте зеленых полей. Общий подход идентичен:

  1. Напишите тест для воспроизведения ошибки (тестовая полоса красного цвета)
  2. Введите код для исправления ошибки (тестовая полоска зеленого цвета)
  3. а затем рефакторинг (тестовая полоса все еще зеленая)

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

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

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

public class CarFactory {
...
public void buildCars(int number, String model, String brand) {
for(int i = 0; i < number; i++) {
String serialNumber;
//
// 20 lines of code to calculate new serial number
//
Car car = new Car(model,brand,serialNumber);
Car.save(car);
}
}
}

 

Класс домена выглядит следующим образом. Логика сохранения встроена в метод save (). Этот метод открывает соединение JDBC и записывает данные в производственную базу данных.

public class Car {
private String model;
private String brand;
private String serialNumber;
...
static public void save(Car car) throws PersistenceException {
//
// JDBC code to connect to the production database and to
// write to the T_CAR table
//
}
}

 

У нас есть довольно хорошая идея, что проблема может быть в методе buildCars (). Но чтобы воспроизвести эту ошибку в модульном тесте, нам нужно было бы вызвать этот метод. В настоящее время вызов этого метода приведет к записи данных непосредственно в базу данных. Кроме того, у нас нет копии базы данных локально, так как она слишком большая. Так что запуск метода по отдельности исключен.

Либо это. Для этого нам нужно иметь возможность заглушить часть CarFactory, которая пишет в базу данных, чтобы мы могли протестировать остальное. Это усложняется тем, что метод save () является статическим. Давайте начнем с небольшого рефакторинга класса CarFactory, чтобы поместить логику персистентности в отдельный класс, называемый CarDao:

public class CarFactory {

private CarDao dao = new DefaultCarDao();

public void setDao(CarDao dao) {
this.dao = dao;
}

public void buildCars(int number, String model, String brand) {
for(int i = 0; i < number; i++) {
String serialNumber = Integer.toString(i);
Car car = new Car(model,brand,serialNumber);
dao.saveCar(car);
}
}
}

Интерфейс CarDao и класс DefaultCarDao могут выглядеть следующим образом:

public interface CarDao {
public void saveCar(Car car) throws PersistanceException;
}

public class DefaultCarDao implements CarDao {

public void saveCar(Car car) throws PersistanceException {
Car.save(car);
}
}

 

Исходный класс CarFactory функционально не изменяется — по умолчанию он создает объект DefaultCarDao, который делает то же самое, что и исходный код. Однако теперь мы можем внедрить макетированный объект CarDao для наших тестов. Мы сломали зависимость. Теперь мы можем приступить к написанию теста, который воспроизводит проблему, связанную с серийными номерами:

public class CarFactoryTest {

@Test
public void carFactoryShouldGenerateTheRightSerialNumber() {
CarFactory factory = new CarFactory();
CarDao mockDao = mock(CarDao.class);
factory.setDao(mockDao);

// I know what the serial number should be
String expectedSerialNumber = "123456";

Car aToyotaPrius = new Car("Toyota", "Prius", expectedSerialNumber);
factory.buildCars(1, "Toyota", "Prius");
verify(mockDao).saveCar(refEq(aToyotaPrius)));

}
}

Мы также можем использовать этот подход, чтобы гарантировать, что класс CarFactory правильно вызывает метод Car.save (). Например, в следующем тесте мы проверяем, что метод buildCars () вызывает Car.save () правильное количество раз и проходит через правильные значения модели и бренда:

public class CarFactoryTest {

@Test
public void carFactoryShouldCreateTheRightNumberOfCars() {
CarFactory factory = new CarFactory();
CarDao mockDao = mock(CarDao.class);
factory.setDao(mockDao);

Car aToyotaPrius = new Car("Toyota", "Prius","");
factory.buildCars(10, "Toyota", "Prius");
verify(mockDao, times(10)).saveCar(refEq(aToyotaPrius, "serialNumber"));

}
}

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

Если вы работаете с унаследованным кодом, в книге Майкла Фезера « Эффективная работа с унаследованным кодом » содержится много полезных советов и рекомендаций в этой области. Я также расскажу, как использовать методы TDD с унаследованным кодом в предстоящем курсе « Тестирование и TDD для разработчиков Java» , который будет проходить в Сиднее и Мельбурне в декабре, а также на других сайтах в следующем году.