Что такое TDD. Создаем валидатор паролей на регулярках
Приветствую всех, в данной статье я кратко расскажу и покажу, что такое TDD на очень простом примере.
Alert!
Данная статья не претендует на тьюториал, или на светоч знаний про методологию. Это скорее шпаргалка, которая была у меня в голове. Простенький пример, которым я решил поделиться.Также статья не удивит никого, кто уже знаком с методологией TDD, это лишь демонстрация основного ее принципа: пиши тесты, до того, как пишешь код.
Итак, концепция TDD (Test Driven Development) — достаточно проста: Разработка ведется короткими циклами, каждый из которых состоит из 3‑х стадий:
1)Написание тестов, покрывающий желаемое изменение
2)Написание кода, который позволит пройти тест
3) Рефакторинг нового кода к соответствующим стандартам, если требуется
TDD Cycle
Теория на этом закончилась, если вам не хватило, вот пару толковых статей: тык и тык.
-------------------------------------------------------------------------------------------------------------------------------------------
Теперь же представим себя разработчиком в вымышленной ИТ компании, перед которым стоит задача: написать валидатор пользовательских паролей, при этом стараясь следовать принципам TDD.
Начнем разработку нашей программы с ознакомления с требованиями службы безопасности:
Придуманный пользователем пароль:
Спецсимволы: @ ! # $ % ^ & * () — _ + = ; : , . / ? \ | ` ~ [ ] { }
Пароль считается слабым, если выполнено хотя бы 1 условие из списка:
Не содержит букв
Имеет длину 8 символов и содержит один символ 3 или более раз
Пароль считается средним, если выполнено хотя бы 1 условие из списка:
Не содержит цифр
Состоит менее чем из 10 символов
Содержит только 1 цифру, которая стоит в конце.
Пароль в остальных случаях считается сильным.
Приступим к написанию тестов.
⚠️ Для наглядности мы будем хранить пароль в переменной типа String, что не является хорошей практикой в реальных проектах ⚠️
Хронологический порядок написания всего кода данного проекта вы можете посмотреть на моем GitHub, кликнув по истории коммитов. Здесь я буду приводить лишь небольшие выдержки из unit-тестов
Первая пачка тестов будет посвящена тому, что бы недопустимым паролям был присвоен статус INCORRECT.
Это будет 3 теста:
1) На длину пароля
2) На проверку соответствию букв в пароле буквам латинского алфавита
3) На содержание как минимум 1‑го спецсимвола.
Ссылка на коммит итерации
Примеры проверок на этой стадии:
assertEquals(PasswordValidator.validatePassword("русскийязык$77"), PasswordStatus.INCORRECT);
assertEquals(PasswordValidator.validatePassword("$7你好754你好"), PasswordStatus.INCORRECT);
assertEquals(PasswordValidator.validatePassword("helloworld"), PasswordStatus.INCORRECT);
assertEquals(PasswordValidator.validatePassword("01122000"), PasswordStatus.INCORRECT);
После написания тестов реализуем все эти проверки во вспомогательном приватном методе passwordIsCorrect()
, и используем его в основном методе validatePassword()
.
Я реализовал данные проверки с помощью регулярных выражений:
public static PasswordStatus validatePassword(String password){
if(!passwordIsCorrect(password)) return PasswordStatus.INCORRECT;
return null;
}
private static boolean passwordIsCorrect(String password){
if(!password.matches("^.{8,22}$")) return false;
if(!password.replaceAll("[\\\\!@#$%^&*()—_+=;:,./?|`~\\[\\]{}\\d]","").matches("^[a-zA-Z]*$")) return false;
if(password.replaceAll("[^\\\\!@#$%^&*()—_+=;:,./?|`~\\[\\]{}]","").equals("")) return false;
return true;
}
first test set completed
После того как тесты прошли, мы можем переходить ко 2‑й итерации, так как рефакторить нам пока что ничего не нужно.
Во второй итерации я напишу тесты, которые уже будут проверять нашу систему оценки сложности пароля.
Суммарно в требованиях 5 критериев, по которым мы присваиваем ту или иную степень надежности паролю, поэтому напишем 5 тестов по этим критериям и еще один дополнительный, для тестирования «эталонных», сложных паролей.
Ссылка на коммит (юнит тесты 2й итерации)
Примеры проверок:
assertEquals(PasswordValidator.validatePassword("1234567#"),PasswordStatus.WEAK);
assertEquals(PasswordValidator.validatePassword("$abc&cbat#^"),PasswordStatus.MEDIUM);
assertEquals(PasswordValidator.validatePassword("2023harl&&ff"),PasswordStatus.STRONG);
Разумеется, все написанные проверки не проходят:
second test set
Я написал простой код (коммит 2й итерации), который последовательно проверяет пароль на соответствие всем критериям с помощью метода replaceAll()
и пачки регулярных выражений:
Доработка метода validatePassword
:
if(password.replaceAll("[^a-zA-Z]","").equals("")) return PasswordStatus.WEAK;
if(password.length()==8 && numberOfOccurrencesOfTheMostCommonCharacterInString(password)>=3) return PasswordStatus.WEAK;
if(password.replaceAll("\\D","").equals("")) return PasswordStatus.MEDIUM;
if(password.length()<10) return PasswordStatus.MEDIUM;
if(password.matches("^\\D*\\d$")) return PasswordStatus.MEDIUM;
return PasswordStatus.STRONG;
Функция, возвращающая число повторов самого часто встречающегося символа в строке:
private static int numberOfOccurrencesOfTheMostCommonCharacterInString(String s){
Map map = new HashMap<>();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
Integer val = map.get(c);
if (val != null) map.put(c, val + 1);
else map.put(c, 1);
}
return Collections.max(map.values());
}
И вот, вуаля! Все тесты проходят.
second test set completed
Казалось бы, что на этом все. Мы написали рабочий код, который проходит все тесты и корректно выполняет свою работу. Но как бы не так. После запуска нашего кода в работу, выяснилось следующее:
1) Иногда случаются сбои, и в нашу программу может прилететь некорректный аргумент, поэтому нужно грамотно обработать null и пробрасывать IllegalArgumentException
2) Служба безопасности прислала нам список 500 самых часто используемых паролей (файл
dangerous_passwords.txt
). Эти пароли взломщики будут использовать в первую очередь, поэтому данным паролям должен быть присвоен статус WEAK.
Итак, приступим к новой итерации.
Напишем 2 теста, первый будет проверять, что IllegalArgumentException
пробрасывается с корректным сообщением ошибки, второй — проверять, что паролям из текстового файла не присваиваются статусы MEDIUM и STRONG.
Ссылка на коммит (юнит тесты 3й итерации) —
Примеры проверок:
@Test
public void passIsNullTest(){
try{
PasswordValidator.validatePassword(null);
fail();
}
catch (IllegalArgumentException e){
assertEquals("Password can't be null", e.getMessage());
}
}
И проверки паролей из «опасного списка»:
@Test
public void passFromDangerousListIsWeak(){
assertEquals(PasswordValidator.validatePassword("tpepsucolia@1209"), PasswordStatus.WEAK);
assertEquals(PasswordValidator.validatePassword("V6#WnsBLDES2!7Zg"), PasswordStatus.WEAK);
}
Запускаем наши тесты, удостоверяемся в том, что они не проходят, и садимся писать код.
Я создал отдельный приватный статический метод (коммит), который будет проверять, является ли входящая строка подмножеством строк файла dangerous_passwords.txt
, а также немного дописал метод проверки пароля на корректность, добавив в него проверку на null:
Доработка основного метода validatePassword
:
if(passwordInDangerousList(password)) return PasswordStatus.WEAK;
Доработка boolean
метода passwordIsCorrect(String password)
:
if(password==null) throw new IllegalArgumentException("Password can't be null");
Вспомогательная функция проверки в файле:
private static boolean passwordInDangerousList(String password){
Scanner scanner;
try {
scanner = new Scanner(new File("src/main/resources/dangerous_passwords.txt"));
} catch (FileNotFoundException e) {
throw new RuntimeException("Can't find file dangerous_passwords.txt");
}
while (scanner.hasNext()){
String dangerousPassword = scanner.next();
if(password.equals(dangerousPassword)) return true;
}
return false;
}
Результат:
11 of 11
Данный проект достаточно прост по своей структуре, поэтому мне не понадобился рефакторинг кода, проходящего тесты, в конце итераций, но в больших проектах рефакторинг, вероятно, потребуется после получения новых требований.
Спасибо за внимание!
Полный код проекта на GitHub: https://github.com/youngmyn/password-validator-TDD
Источники: