Kata 06 in TypeScript: Password Validator

Published on
8 mins read
Series: Sustainable Testing Katas in TypeScript

The katas of this series are proposed exercises in the excellent course Testing Sostenible con TypeScript by Miguel A. Gómez and Carlos Blé

Introduction

In this article, we will see how to use TDD and Jest to implement a function that validates if a password is sufficiently strong according to the given criteria.

Table of Contents

Statement

In this exercise we will program a boolean function that indicates if a given password complies with some strength requirements. For the function to produce a true result, the password must:

  • Have a length of at least six characters
  • Contain some number
  • Contain some lowercase letter
  • Contain some uppercase letter
  • Contain some underscore

Examples

  • StRonG_92bC \Rightarrow true - meets all criteria
  • abc \Rightarrow false - isn't long enough
  • ABCdef_ \Rightarrow false - has no numbers
  • ABCDEF_1 \Rightarrow false - has no lowercase letters
  • abcdef_1 \Rightarrow false - has no uppercase letters
  • Abcdef1 \Rightarrow false - has no underscores

Creating the validator

Unlike other katas, this time we will need to use a different approach because a valid password has to meet all the requirements at the same time. Then, we cannot create tests which expect a valid password for each case, because it would have to comply with the rest so that the tests remain valid. To do this would be to repeat the same test, that the password is valid.

Because of this, we need to use the opposite approach, testing that the password is not valid for each case using passwords that don't comply only with this case, to make sure that we are testing an isolated case and if it fails, it is because of a poor implementation.

Clearly, there must be a test expecting a valid password. We will start with the latter case and implement the rest as mentioned in the statement. Why? Because by already having a test for a valid password, we can focus on creating the tests and implementations as if they were blocks to build the complete solution that covers all the cases.

Case: valid password

First, let's create two files: password-validator.ts and password-validator.test.ts. The first one will contain the isStrongPassword function and the second, the tests.

// src/core/password-validator.ts
export function isStrongPassword(input: string) {
    // implementation goes here
}
// src/tests/password-validator.test.ts
import {isStrongPassword} from "../core/password-validator";

To group our tests, let's define a suite containing Password Validator as description.

// src/tests/password-validator.test.ts
describe("Password Validator", () => {
    // tests go here
});

Now, let's create the test for the case of a valid password.

// src/tests/password-validator.test.ts
it('considers a password to be strong when all requirements are met', () => {
    expect(isStrongPassword('StRonG_92bC')).toBe(true);
});

The minimum implementation would be to simply return true.

// src/core/password-validator.ts
export function isStrongPassword(input: string) {
    return true;
}

There's nothing to refactor.

Case: password too short

We create the test:

// src/tests/password-validator.test.ts
it('fails when the password is too short', () => {
    expect(isStrongPassword('abc')).toBe(false);
});

The minimum implementation would be to return the result of evaluating if the length of the password is greater than or equal to 6. If it is, then the password is valid. Otherwise, it isn't.

// src/core/password-validator.ts
export function isStrongPassword(password: string) {
    return password.length >= 6;
}

There's nothing to refactor.

Case: password with no numbers

We create the test:

// src/tests/password-validator.test.ts
it('fails when the password is missing a number', () => {
    expect(isStrongPassword('ABCdef_')).toBe(false);
});

The minimum implementation would be to use a regular expression that searches for numbers in the password. This validation is nested to the previous one.

// src/core/password-validator.ts
export function isStrongPassword(password: string) {
    return password.length >= 6 && /\d/g.test(password);
}

Let's use this approach to add the rest of the validity criteria. Nesting each criterion as a condition. A valid password will be the one that complies with them all. Now it's time to refactor. Since the regular expressions are hard to read, it is always a good idea to extract them to their own function, and since we are here, let's extract the length validation too, placing the minimum length in a constant to facilitate its modification in case the criterion changes.

// src/core/password-validator.ts
const MINIMUM_CHARACTER_LENGTH = 6;

export function isStrongPassword(password: string) {
    return isMinimumLength(password) && containsNumber(password);
}

function isMinimumLength(password: string) {
    return password.length >= MINIMUM_CHARACTER_LENGTH;
}

function containsNumber(password: string) {
    return /\d/g.test(password);
}

Case: password with no lowercase letters

We create the tests:

// src/tests/password-validator.test.ts
it('fails when the password is missing a lowercase', () => {
    expect(isStrongPassword('ABCDEF_1')).toBe(false);
});

The minimum implementation would be to use a regular expression that searches for lowercase letters in the password.

// src/core/password-validator.ts
const MINIMUM_CHARACTER_LENGTH = 6;

export function isStrongPassword(password: string) {
    return isMinimumLength(password) && containsNumber(password) && /[a-z]/g.test(password);
}

function isMinimumLength(password: string) {
    return password.length >= MINIMUM_CHARACTER_LENGTH;
}

function containsNumber(password: string) {
    return /\d/g.test(password);
}

In the same way, we refactor and extract this new validation to its own function.

// src/core/password-validator.ts
const MINIMUM_CHARACTER_LENGTH = 6;

export function isStrongPassword(password: string) {
    return isMinimumLength(password) && containsNumber(password) && containsLowerCase(password);
}

function isMinimumLength(password: string) {
    return password.length >= MINIMUM_CHARACTER_LENGTH;
}

function containsNumber(password: string) {
    return /\d/g.test(password);
}

function containsLowerCase(password: string) {
    return /[a-z]/g.test(password);
}

Case: password with no uppercase letters

We create the tests:

// src/tests/password-validator.test.ts
it('fails when the password is missing an uppercase', () => {
    expect(isStrongPassword('abcdef_1')).toBe(false);
});

The minimum implementation would be very similar to the previous one, but now instead of lowercase letters, we use uppercase letters.

// src/core/password-validator.ts
const MINIMUM_CHARACTER_LENGTH = 6;

export function isStrongPassword(password: string) {
    return isMinimumLength(password) && containsNumber(password) && containsLowerCase(password) && /[A-Z]/g.test(password);
}

function isMinimumLength(password: string) {
    return password.length >= MINIMUM_CHARACTER_LENGTH;
}

function containsNumber(password: string) {
    return /\d/g.test(password);
}

function containsLowerCase(password: string) {
    return /[a-z]/g.test(password);
}

Let's refactor in the same way, extracting the new validation to its own function. Moreover, as the line is already getting too long, let's place each validation in their own line so that the function is more readable.

// src/core/password-validator.ts
const MINIMUM_CHARACTER_LENGTH = 6;

export function isStrongPassword(password: string) {
    return isMinimumLength(password)
        && containsNumber(password)
        && containsLowerCase(password)
        && containsUpperCase(password);
}

function isMinimumLength(password: string) {
    return password.length >= MINIMUM_CHARACTER_LENGTH;
}

function containsNumber(password: string) {
    return /\d/g.test(password);
}

function containsLowerCase(password: string) {
    return /[a-z]/g.test(password);
}

function containsUpperCase(password: string) {
    return /[A-Z]/g.test(password);
}

Case: password with no underscores

We come to the last case. Following the same process, we create the test:

// src/tests/password-validator.test.ts
it('fails when the password is missing an underscore', () => {
    expect(isStrongPassword('Abcdef1')).toBe(false);
});

The minimum implementation would be to use a regular expression which searches for underscores in the password, as in the case of the numbers.

// src/core/password-validator.ts
const MINIMUM_CHARACTER_LENGTH = 6;

export function isStrongPassword(password: string) {
    return isMinimumLength(password)
        && containsNumber(password)
        && containsLowerCase(password)
        && containsUpperCase(password)
        && /_/g.test(password);
}

function isMinimumLength(password: string) {
    return password.length >= MINIMUM_CHARACTER_LENGTH;
}

function containsNumber(password: string) {
    return /\d/g.test(password);
}

function containsLowerCase(password: string) {
    return /[a-z]/g.test(password);
}

function containsUpperCase(password: string) {
    return /[A-Z]/g.test(password);
}

Let's do the last refactoring and extract the new validation to its own function.

// src/core/password-validator.ts
const MINIMUM_CHARACTER_LENGTH = 6;

export function isStrongPassword(password: string) {
    return isMinimumLength(password)
        && containsNumber(password)
        && containsLowerCase(password)
        && containsUpperCase(password)
        && containsUnderscore(password);
}

function isMinimumLength(password: string) {
    return password.length >= MINIMUM_CHARACTER_LENGTH;
}

function containsNumber(password: string) {
    return /\d/g.test(password);
}

function containsLowerCase(password: string) {
    return /[a-z]/g.test(password);
}

function containsUpperCase(password: string) {
    return /[A-Z]/g.test(password);
}

function containsUnderscore(password: string) {
    return /_/g.test(password);
}

That's it! With this our isStrongPassword function would be ready. The tests give us the assurance that it covers all the specified criteria.

You can find this kata, and the rest of them, here.

Conclusion

At least I think that, without using the TDD approach, it is more difficult to get to a solution as simple as this. It is perfectly possible to do it, but it is often harder because one tends to want to solve the problem as a whole instead of breaking it down into parts and building the solution piecemeal.

I hope this other example of solving problems by thinking about testing first helps you better understand the essence of TDD. You know the drill, if you have a question or want to share something, leave it in the comments :)