Что такое TDD. Создаем валидатор паролей на регулярках

Приветствую всех, в данной статье я кратко расскажу и покажу, что такое TDD на очень простом примере.

Alert!
Данная статья не претендует на тьюториал, или на светоч знаний про методологию. Это скорее шпаргалка, которая была у меня в голове. Простенький пример, которым я решил поделиться.

Также статья не удивит никого, кто уже знаком с методологией TDD, это лишь демонстрация основного ее принципа: пиши тесты, до того, как пишешь код.

Итак, концепция TDD (Test Driven Development) — достаточно проста: Разработка ведется короткими циклами, каждый из которых состоит из 3‑х стадий:

1)Написание тестов, покрывающий желаемое изменение

2)Написание кода, который позволит пройти тест

3) Рефакторинг нового кода к соответствующим стандартам, если требуется

TDD Cycle

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

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

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

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

11 of 11

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

Спасибо за внимание!

Полный код проекта на GitHub: https://github.com/youngmyn/password-validator-TDD

Источники:

© Habrahabr.ru