What is TDD. Creating a password validator using regular expressions

Hello everyone, in this article I will briefly tell and show what TDD is using a very simple example.

Alert!
This article does not pretend to be a tutorial or a beacon of knowledge about the methodology. It is rather a cheat sheet that was in my head. A simple example that I decided to share.

Also, the article will not surprise anyone who is already familiar with the TDD methodology, it is just a demonstration of its main principle: write tests before you write code.

So, the concept of TDD (Test Driven Development) is quite simple: Development is carried out in short cycles, each of which consists of 3 stages:

1)Writing tests that cover the desired change

2) Writing code that will allow you to pass the test

3) Refactoring new code to the relevant standards, if required

TDD Cycle

TDD Cycle

The theory ends here, if you didn't get enough, here are a couple of useful articles: poke And poke.

————————————————– ————————————————– —————————————

Now let's imagine ourselves as a developer in a fictional IT company, who is faced with the task of writing a user password validator, while trying to follow the principles of TDD.

Let's start developing our program by familiarizing ourselves with the security service requirements:

User-created password:

Special characters: @ ! # $ % ^ & * ( ) — _ + = ; : , . / ? \ | ` ~ [ ] { }

A password is considered weak if at least 1 condition from the list is met:

  • Does not contain letters

  • Is 8 characters long and contains the same character 3 or more times

A password is considered average if at least 1 condition from the list is met:

  • Does not contain numbers

  • Consists of less than 10 characters

  • Contains only 1 digit, which is at the end.

The password is considered strong in other cases.

Let's start writing tests.

⚠️ For clarity, we will store the password in a String variable, which is not a good practice in real projects. ⚠️

You can see the chronological order of writing all the code for this project on my GitHubby clicking on the commit history. Here I will only provide small excerpts from unit tests

The first batch of tests will be dedicated to ensuring that invalid passwords are assigned the status INCORRECT.

There will be 3 tests:

1) On the length of the password

2) To check that the letters in the password correspond to the letters of the Latin alphabet

3) Contains at least 1 special character.

Link to iteration commit

Examples of checks at this stage:

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);

After writing the tests, we implement all these checks in an auxiliary private method passwordIsCorrect()and we use it in the main method validatePassword().

I implemented these checks using regular expressions:

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

After the tests have passed, we can move on to the 2nd iteration, since we don’t need to refactor anything yet.

In the second iteration I will write tests that will check our password complexity assessment system.

In total, the requirements contain 5 criteria by which we assign a certain level of reliability to a password, so we will write 5 tests according to these criteria and one additional one for testing “standard”, complex passwords.

Link to commit (unit tests of the 2nd iteration)

Examples of checks:

assertEquals(PasswordValidator.validatePassword("1234567#"),PasswordStatus.WEAK);
assertEquals(PasswordValidator.validatePassword("$abc&cbat#^"),PasswordStatus.MEDIUM);
assertEquals(PasswordValidator.validatePassword("2023harl&&ff"),PasswordStatus.STRONG);

Of course, all the written tests fail:

second test set

second test set

I wrote a simple code (commit 2nd iteration), which sequentially checks the password against all criteria using the method replaceAll() and packs of regular expressions:

Refinement of the method 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;

A function that returns the number of occurrences of the most frequently occurring character in a string:

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());
}

And voila! All tests pass.

Second test set completed

Second test set completed

It would seem that this is all. We have written working code that passes all tests and does its job correctly. But it is not so. After running our code, the following became clear:

1) Sometimes failures occur, and an incorrect argument can fly into our program, so we need to correctly handle null and throw IllegalArgumentException

2) The security service sent us a list of the 500 most frequently used passwords (file dangerous_passwords.txt). These are the passwords that attackers will use first, so these passwords should be assigned the WEAK status.

So, let's move on to a new iteration.

Let's write 2 tests, the first one will check that IllegalArgumentException is passed with the correct error message, the second one is to check that passwords from the text file are not assigned the MEDIUM and STRONG statuses.

Link to commit (unit tests 3rd iteration) –

Examples of checks:

@Test
public void passIsNullTest(){
    try{
        PasswordValidator.validatePassword(null);
        fail();
    }
    catch (IllegalArgumentException e){
        assertEquals("Password can't be null", e.getMessage());
    }
}

And checks for passwords from the “dangerous list”:

@Test
public void passFromDangerousListIsWeak(){
    assertEquals(PasswordValidator.validatePassword("tpepsucolia@1209"), PasswordStatus.WEAK);
    assertEquals(PasswordValidator.validatePassword("V6#WnsBLDES2!7Zg"), PasswordStatus.WEAK);
}

We run our tests, make sure they fail, and sit down to write code.

I created a separate private static method (commit), which will check if the input line is a subset of the lines in the file dangerous_passwords.txtand also slightly modified the method for checking the password for correctness, adding a check for null:

Refinement of the basic method validatePassword :

 if(passwordInDangerousList(password)) return PasswordStatus.WEAK;  

Refinement boolean method passwordIsCorrect(String password):

 if(password==null) throw new IllegalArgumentException("Password can't be null");

Helper function for checking in file:

 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;
}

Result:

11 of 11

11 of 11

This project is simple enough in structure that I didn't need to refactor the code that passes the tests at the end of iterations, but in larger projects refactoring will likely be required as new requirements are received.

Thank you for your attention!

Full project code on GitHub: https://github.com/youngmyn/password-validator-TDD

Sources:

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *