- Kata 04 in TypeScript: Video Surveillance - Part 2
- Kata 05 in TypeScript: String Calculator
- Kata 06 in TypeScript: Password Validator
- Kata 07 in TypeScript: CSV Filter
- Kata 08 in TypeScript: Fibonacci
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
true
- meets all criteriaabc
false
- isn't long enoughABCdef_
false
- has no numbersABCDEF_1
false
- has no lowercase lettersabcdef_1
false
- has no uppercase lettersAbcdef1
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.
Link to GitHub repository
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 :)