В жизненном цикле разработки программного обеспечения модульное тестирование играет жизненно важную роль. Оно похоже на проверку качества компонентов в процессе производства автомобилей, обеспечивая соответствие каждого компонента установленным стандартам. Очевидно, что нельзя игнорировать необходимость модульного тестирования. Только когда каждый программный элемент проходит строгий контроль качества, вся система может работать стабильно. В противном случае любые основные проблемы могут привести к неисправности всей системы.
В индустрии программного обеспечения в целом согласны с тем, что модульное тестирование является ключевым шагом в улучшении качества и точности кода. Однако в реальной разработке проекта по-прежнему распространено отсутствие модульного тестирования или низкий охват, что напрямую приводит к тому, что многие ошибки обнаруживаются только после запуска продукта, что не только задерживает ход проекта, но и часто вызывает проблемы в Интернете. Так почему же, несмотря на широкое признание его ценности, модульное тестирование на практике все еще часто игнорируется?
За мое участие в проекте,В некоторых проектах полностью отсутствуют модульные тесты.,И большинство разработчиков склонныmain
Пишите тестовый код прямо в методе,На самом деле это отражает пренебрежение разработчиком модульным тестированием. Даже если некоторые проекты содержат небольшое количество модульных тестов,Также часто ограничивается простыми инструментами или статическими методами.,Тестовые случаи просты и не имеют достаточного покрытия.,Особенно для сложной обработки зависимостей.
В совокупности существует три основные причины отказа от написания модульных тестов: во-первых, у младших разработчиков недостаточно знаний о модульных тестах; во-вторых, у разработчиков ограниченные навыки написания модульных тестов; в-третьих, плохая тестируемость самого кода и написание модульного тестирования; сталкивается с огромными проблемами. Что касается первых двух пунктов, осведомленность и навыки разработчиков необходимо улучшать посредством обучения, а проблемы тестируемости кода можно улучшить с помощью принципов проектирования;
Давайте сначала рассмотрим первый принцип написания тестируемого кода: неопределенные входные данные. Предположим, существует метод определения того, является ли текущий год високосным. Он не имеет параметров и возвращает логическое значение. Этот метод сначала получает текущее время, а затем на основе года определяет, является ли этот год високосным. если год 4 Кратное число — високосный год. В противном случае это не високосный год.
public boolean isLeapYear(){
Date date = new Date();
int year = 1900 + date.getYear();
return year % 4 == 0;
}
Хотя этот метод прост, писать модульные тесты непросто, поскольку он зависит от текущего времени, а текущее время является неопределенным и зависит от того, когда вы запускаете модульный тест. Тестируемость этого метода плохая. Представьте себе: если вы хотите проверить будущее время, вам остается только ждать. Если вы хотите проверить время в прошлом, вы можете использовать машину времени только для того, чтобы вернуться в прошлое. Другой способ — изменить системное время. Перед запуском каждого модульного теста измените текущее системное время. Хотя это можно проверить в любое время, это увеличит стоимость тестирования.
Это приводит к принципу написания тестируемого кода: неопределенные входные данные. На основе этого принципа,Внесите некоторые изменения в код. Мы можем убрать логику получения текущей даты из метода,и добавьте текущий год из параметра в метод,Это позволяет легко проводить тестирование в любой год.
public boolean isLeapYear(int year){
return year % 4 == 0;
}
Возможно, вы заметили, что оценка високосного года только на основании того, кратен ли год 4, является неточной. Метод расчета високосных лет таков: каждые четыре года происходит скачок, каждые сто лет нет скачка и каждые четыреста лет делается еще один скачок. Например, 2008 год — високосный, 1900 — не високосный, а 2000 — високосный, поэтому при написании модульных тестов вам необходимо охватить эти особые годы, которые являются граничными значениями. Типичные граничные значения включают нулевое значение, нулевое значение, нулевое значение и т. д. Возьмитесь за доску и напишите модульные тесты, чтобы охватить граничные значения.
Второй принцип написания тестируемого кода — программировать с использованием абстракций, а не реализаций. На самом деле это принцип объектно-ориентированного программирования. Это может улучшить масштабируемость кода и позволить нам гибко заменять определенные реализации. Аналогично, этот принцип может улучшить тестируемость вашего кода.
Вы можете придумать пример: есть программа-сканер, которая сканирует информацию о продуктах на Taobao. Если она обнаруживает, что доступ к странице Taobao невозможен, она повторяет попытку три раза с интервалом в 10 секунд каждый раз. Если вы младший разработчик, вы можете напрямую использовать новое ключевое слово для создания HttpClient, а затем использовать его для доступа к Taobao. Такой код достаточно прост и может обеспечить функциональность. Итак, предположим, вам нужно написать для него модульный тест, чтобы убедиться, что при сбое доступа к Taobao он будет повторяться до трех раз с интервалом в 10 секунд каждый раз. Сейчас вы обнаружите, насколько сложно писать для него модульные тесты.
Ключевой вопрос, с которым вы сталкиваетесь: как заставить доступ к Taobao возвращать ошибку 500? Это действительно проблема века. Вы можете позвонить Джеку Ма и сказать ему, что мы проводим модульное тестирование, и попросить его сотрудничать и временно закрыть Taobao на несколько минут, а затем восстановить его после того, как мы закончим тестирование. Конечно, нам не нужно беспокоить господина Ма. Мы также можем настроить локальный DNS так, чтобы доменное имя Taobao указывало на неправильный IP-адрес, или изменить код HttpClient для выполнения специальной обработки запросов Taobao.
Вы узнали? Эту программу-сканер практически невозможно протестировать. Основная причина заключается в том, что она создает конкретную реализацию HttpClient посредством new. Она запрограммирована для конкретных реализаций, а не для абстрактного программирования.
Фактически почти все проекты будут иметь такой код. Например, используйте new в конструкторе для создания конкретной реализации и создайте новую локальную переменную в методе. Когда вы обнаружите, что ваш код сложно тестировать из-за использования новых интерфейсов, вам следует рассмотреть возможность замены их абстрактными интерфейсами. Обычный код необходимо запускать в производственной среде, но в контексте модульного тестирования среда, в которой выполняется код, отличается. Для этого необходимо, чтобы код был основан на абстракции, которая при запуске в продакшене использует обычное окружение, а при запуске в модульных тестах его можно заменить специальной реализацией, удобной для тестирования. Этот метод называется издевательством, и я объясню его ниже. Теперь не забудьте сосредоточиться на абстракциях, а не на реализациях. Это основа для написания тестируемого кода.
Теперь мы понимаем два принципа написания тестируемого кода: неопределенные входные данныеи Абстрактно-ориентированное программирование. Конечно, на тестируемость кода влияет множество факторов. Я считаю, что после соблюдения этих двух принципов вы сможете писать тестируемый код. Код уже доступен для тестирования, так как же писать модульные тесты? Теперь я расскажу вам о некоторых методах написания модульных тестов, в основном Mock Использование рамок.
Только что мы привели пример оценки високосных годов. Он относительно прост, имеет простые входные и выходные данные и не имеет никаких других зависимостей. Но в реальных сценариях зачастую все сложнее. Между классами существуют взаимные зависимости, а также зависимости от некоторых фреймворков, баз данных, кешей, очередей сообщений и т. д. Это создает огромные проблемы при написании тестируемого кода и модульных тестов. Далее мы приведем пример приложения с классической трехуровневой архитектурой, использующий среду Spring MVC, чтобы проиллюстрировать, как писать модульные тесты в реальном проекте.
Давайте посмотрим на этот код, предположив, что существует пользовательский класс обслуживания, который является Spring Bean. Частная переменная UserDao вводится через @Autowired, которая используется для управления базой данных. Класс Service имеет метод сохранения, который вызывает метод вставки объекта DAO. Первый параметр — это идентификатор пользователя, а второй параметр — это строка, объединяющая имя и фамилию пользователя.
@Service
public class UserService{
@Autowired
private IUserDao userDao;
public void save(User user){
userDao.insert(user.id, user.firstName + " " + user.lastName());
}
}
Это особенно классический пример, который можно увидеть практически в каждом проекте, использующем среду Spring. Так как же протестировать такой класс?
Прежде чем писать модульные тесты, нам сначала нужно ответить на три основных вопроса о модульных тестах:
Первый вопрос: что измеряет модульное тестирование? Если метод не возвращает значение, что именно мы тестируем?
Второй вопрос: если у класса есть внешние зависимости, даже если логика текущего класса правильна, если во внешнем классе есть ошибка, это приведет к неправильной работе текущего класса. Поэтому, как писать модульные тесты. справиться с ситуацией, когда зависимое поведение не соответствует ожиданиям?
Третья проблема: тестируемый класс зависит от среды Spring и базы данных. Как запустить контейнер Spring и базу данных при запуске модульных тестов?
Эти три проблемы беспокоят многих разработчиков. Если у вас тоже есть такие сомнения, пожалуйста, внимательно послушайте ниже.
Первый вопрос заключается в том, что модульное тестирование предназначено для проверки того, соответствует ли поведение класса ожидаемому. У класса много вариантов поведения, и возвращаемое значение метода — только одно из них. Другие варианты поведения включают работу с базой данных, вызов других. сервисы, выдача исключений и т. д. В реальных проектах обычно проверяется, правильно ли класс вызывает другие зависимости, а также соответствуют ли параметры и время вызова ожидаемым.
Второй вопрос заключается в том, что для класса с внешними зависимостями модульное тестирование должно гарантировать, что «когда все зависимости класса могут работать нормально, тестируемый класс может работать нормально». Таким образом, существует основное предварительное условие для написания модульных тестов: «все зависимости класса корректны».
В-третьих, модульное тестирование не может запустить контейнер Spring или подключиться к базе данных. Запуск контейнера Spring и подключение к базе данных необходимы на этапе интеграционного тестирования.
Теперь, когда мы решили эти три проблемы, давайте подумаем, как написать модульный тест.
Сначала убедитесь, что метод сохранения вызывает метод вставки объекта DAO и вызывает его только один раз, а параметры представляют собой объединенные строки идентификатора, имени и фамилии по порядку. Это ожидаемое поведение. Во-вторых, модульные тесты не могут запустить контейнер Spring или подключиться к базе данных.
Если он не запускается Spring Контейнер,Пользователь Дао не может быть инициализирован, его значение равно Нулевой. Конечно, мы можем написать его специально для тестирования. UserDao реализация. Но как утверждать insert Метод выполняется один раз, параметры верны? На этот раз вам нужно Использование Mock-фреймворка Понятно。
Mock — это метод, часто используемый при модульном тестировании. Mock означает «поддельный». Он может генерировать поддельный объект на основе интерфейса или класса. А поддельные объекты можно заглушить (также называемое заглушкой). Например, если входным параметром метода является «что», возвращаемым значением будет «что». Мы также можем узнать, был ли выполнен метод поддельного объекта, сколько раз он выполнялся и каковы параметры выполнения. Видите ли, технология Mock как раз отвечает нашим потребностям.
Далее мы используем технологию Mock для написания модульных тестов.
Шаг 1. Создайте экземпляр тестируемого объекта, который является новым UserService.
Шаг 2. Создайте объект Mock, который будет имитировать поддельный объект UserDao, и передайте его в UserService.
Шаг 3. Сложите поддельный объект, то есть что делать при вызове метода вставки поддельного объекта. Здесь нечего делать.
Шаг 4. Сделайте утверждение для поддельного объекта, чтобы определить, выполняется ли метод вставки поддельного объекта и соответствуют ли параметры ожиданиям.
Большинство языков имеют зрелые фреймворки Mock. Если вы используете Java, я рекомендую вам использовать фреймворк Mockito. Он имеет полные функции и относительно дружественный API. Большинство фреймворков с открытым исходным кодом, включая Spring, используют его для модульного тестирования. Согласно 4 шагам, которые я только что упомянул, модульное тестирование можно легко выполнить с помощью любой Mock-инфраструктуры. Вы можете обратиться к коду, который я вам дал.
// Первым шагом является создание тестового объекта.
UserService userService = new UserService();
// Второй шаг – создание Mock объект
IUserDao mockDao = mock(IUserDao.class);
// Шаг 3,ЛОЖЬобъект Выполнить укладку
when(mockDao).insert(anyString(),anyString()).doNothing();
... Use reflection to inject mockDao into userService
// Шаг 4,ЛОЖЬобъект Сделать утверждение
userService.save(new User("123", "hello", "world"));
verify(mockDao).insert(eq("123", eq("hello world")));
Теперь у нас есть Использование Мок-фреймворк, готово UserService модульные тесты. Это все? Вы заметили, что мы оставили небольшую проблему?
UserService использует @Autowired для внедрения зависимостей, то есть внедрения полей. Я считаю, что большинство разработчиков будут использовать этот метод для внедрения зависимостей, поскольку код проще и достаточно добавить аннотацию @Autowired. Но если вы будете осторожны, вы обнаружите, что IDEA выдаст большое предупреждение, указывающее, что внедрение поля не рекомендуется и следует использовать внедрение конструктора.
Знаешь почему? Очевидно, что добавление @Autowired может завершить внедрение. Если вы используете внедрение конструктора, вам нужно написать гораздо больше кода. На собеседовании я задавал этот вопрос многим кандидатам, но не многие смогли на него ответить. Знаете почему? Почему IDEA не рекомендует внедрение Spring в поле?
На самом деле, ответ уже был дан в примере только что. Внедрение полей может привести к тому, что классы будут сильно зависеть от Spring рамка. если ты поместишь все Spring Связанные аннотации, такие как @Service, @Autowired. Удалите их все, и вы обнаружите, что проиграли. Spring поддерживается UserService Существует серьезная проблема, и она заключается в том, что нет способа справиться с ней. private задания на местах, а это значит, что они всегда будут Нулевой. Единственный способ присвоить значение — использовать отражение в разделе «Использование». Mock-фреймворкwhen,нуждатьсяиспользоватьотражение будет ложнымобъектназначен на UserService из private Поле,Повышенная сложность теста,Снижен добрыйиз тестируемости.
Если вы используете внедрение конструктора, у вас не возникнет этой проблемы. можно передать через конструктор Mock объект переходит к реальному объекту. использовать Инъекцию конструктораиз UserService, даже если все Spring Удалите все аннотации, и оно все равно будет правильным. POJO класс, может работать самостоятельно. это не соответствует Spring Сильная связь, просто Spring рамка помогла нам вызвать ее конструктор и передать правильные параметры.
Для этой статьи я нарисовал интеллект-карту, чтобы обобщить ее для читателей.
наконец,Я хотел бы попросить вас задуматься над вопросом: все ли коды нужно тестировать? Поскольку модульное тестирование может улучшить корректность кода,Должны ли мы писать модульные тесты для всего кода? Обычно,Не так. первый,Написание модульных тестов само по себе также требует времени.,Не нулевая стоимость. Во-вторых,Для тех очень простых, маловероятных изменений или одноразового кода,Написание модульных тестов менее важно.