Приветствую всех, в данной статье я кратко расскажу и покажу, что такое TDD на очень простом примере.
Alert!
Данная статья не претендует на тьюториал, или на светоч знаний про методологию. Это скорее шпаргалка, которая была у меня в голове. Простенький пример, которым я решил поделиться.Также статья не удивит никого, кто уже знаком с методологией TDD, это лишь демонстрация основного ее принципа: пиши тесты, до того, как пишешь код.
Итак, концепция TDD (Test Driven Development) – достаточно проста: Разработка ведется короткими циклами, каждый из которых состоит из 3‑х стадий:
1)Написание тестов, покрывающий желаемое изменение
2)Написание кода, который позволит пройти тест
3) Рефакторинг нового кода к соответствующим стандартам, если требуется
Теория на этом закончилась, если вам не хватило, вот пару толковых статей: тык и тык.
-------------------------------------------------------------------------------------------------------------------------------------------
Теперь же представим себя разработчиком в вымышленной ИТ компании, перед которым стоит задача: написать валидатор пользовательских паролей, при этом стараясь следовать принципам TDD.
Начнем разработку нашей программы с ознакомления с требованиями службы безопасности:
Придуманный пользователем пароль:
Не должен быть короче 8 и длиннее 22 символов
Содержит буквы исключительно латинского алфавита (если вообще содержит)
Обязательно содержит хотя бы 1 спецсимвол
Спецсимволы: @ ! # $ % ^ & * ( ) — _ + = ; : , . / ? \ | ` ~ [ ] { }
Пароль считается слабым, если выполнено хотя бы 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;
}
После того как тесты прошли, мы можем переходить ко 2‑й итерации, так как рефакторить нам пока что ничего не нужно.
Во второй итерации я напишу тесты, которые уже будут проверять нашу систему оценки сложности пароля.
Суммарно в требованиях 5 критериев, по которым мы присваиваем ту или иную степень надежности паролю, поэтому напишем 5 тестов по этим критериям и еще один дополнительный, для тестирования «эталонных», сложных паролей.
Ссылка на коммит (юнит тесты 2й итерации)
Примеры проверок:
assertEquals(PasswordValidator.validatePassword("1234567#"),PasswordStatus.WEAK);
assertEquals(PasswordValidator.validatePassword("$abc&cbat#^"),PasswordStatus.MEDIUM);
assertEquals(PasswordValidator.validatePassword("2023harl&&ff"),PasswordStatus.STRONG);
Разумеется, все написанные проверки не проходят:
Я написал простой код (коммит 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<Character, Integer> 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());
}
И вот, вуаля! Все тесты проходят.
Казалось бы, что на этом все. Мы написали рабочий код, который проходит все тесты и корректно выполняет свою работу. Но как бы не так. После запуска нашего кода в работу, выяснилось следующее:
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;
}
Результат:
Данный проект достаточно прост по своей структуре, поэтому мне не понадобился рефакторинг кода, проходящего тесты, в конце итераций, но в больших проектах рефакторинг, вероятно, потребуется после получения новых требований.
Спасибо за внимание!
Полный код проекта на GitHub: https://github.com/youngmyn/password-validator-TDD
Источники:
https://thecode.media/tdd - краткое описание методологий TDD и BDD
https://fortegrp.com/insights/test-driven-development-benefits - неплохая англоязычная статья про TDD
https://javarush.com/groups/posts/6-chto-takoe-tdd-i-moduljhnoe-testirovanie- - про модульное тестирование для начинающих
Комментарии (8)
Kahelman
28.08.2024 22:32+11234567890Password
Это сильный пароль по вашим оценкам.
Как пример для TDD может конечно пойти, но лучше было бы выбрать тему побезопасней.
У меня и так половина сайтов ругается что им то цифры, то символа не хватает в моих 16 символьных паролях сгенерированных password manager-ом. …
Давайте будем писать статьи про «нормальные» пароли исходя из реальных рекомендаций.
Если надо проверить пароль то надо проверять по базам хешей, а не по количеству символов.
youngmyn Автор
28.08.2024 22:32Конкретно этот пароль было бы логично держать в файле dangerous_passwords.txt
Но вообще я не проводил ресерча по настоящим методам валидаций паролей, я понимаю, что там все сложнее, чем : 8 символов и спецзнак. Тут скорее я использовал стереотип, который был у меня в голове для примера. Я действительно брал эти условия с потолка. Прям совсем с потолка.
SuperKozel
28.08.2024 22:32+4Ахахах, только на таких примерах все это тдд и работает, когда одна-две простых функции. Удачи делать тдд когда делается фича с интеграцией, емэйлами и другими сообщениями и событиями.
aProger
28.08.2024 22:32+2ИМХО, задача тестов, при TDD - это более правильно и корректно сформулировать задачу для разработчика функционала.
Kahelman
28.08.2024 22:32У нас тут как раз такой тест: более правильно сформулировать задачу.
В итоге автор реализовал проверку паролей с помощью regexp, и как мы вроде все согласились результат не очень. И надо было по списку общеизвестных паролей пройти.
Как TTD помог в решении проблемы?
SergeiMinaev
Как часто вам приходится писать валидаторы паролей? Вариант пореалистичнее: прототип какого-нибудь дашборда, который заказчик посмотрит, пощупает, выдаст правки, потом снова правки и в последующие 2-3 месяца всё будет переделано вдоль и поперёк.
youngmyn Автор
Давно вынашивал схожие сомнения на этот счет.
Тезисно выразил в статье : https://habr.com/ru/articles/839658/