Приветствую всех, в данной статье я кратко расскажу и покажу, что такое 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- — про модульное тестирование для начинающих
ссылка на оригинал статьи https://habr.com/ru/articles/839404/
Добавить комментарий