Kata 06 con TypeScript: Validador de Contraseña
- Publicado el
- Kata 04 con TypeScript: Videovigilancia - Parte 2
- Kata 05 con TypeScript: Calculadora de Texto
- Kata 06 con TypeScript: Validador de Contraseña
- Kata 07 con TypeScript: Filtro CSV
- Kata 08 con TypeScript: Fibonacci
Las katas de esta serie son ejercicios propuestos en el excelente curso Testing Sostenible con TypeScript de Miguel A. Gómez y Carlos Blé
Introducción
En este artículo, veremos cómo utilizar TDD y Jest para implementar una función que valida si una contraseña es suficientemente fuerte según los criterios dados.
Tabla de Contenido
Enunciado
En este ejercicio vamos a programar una función booleana que indica si una contraseña dada cumple con unos requisitos de fortaleza. Para que la función produzca un resultado verdadero, la contraseña debe de:
- Tener una longitud de al menos seis caracteres
- Contener algún número
- Contener alguna letra mayúscula
- Contener alguna letra minúscula
- Contener algún guion bajo
Ejemplos
StRonG_92bC
true
- cumple todas las reglasabc
false
- no tiene longitud suficienteABCdef_
false
- no tiene númerosABCDEF_1
false
- no tiene minúsculasabcdef_1
false
- no tiene mayúsculasAbcdef1
false
- no tiene guion bajo
Creación del validador
A diferencia de otras katas, en esta ocasión necesitamos utilizar un enfoque diferente porque una contraseña válida debe cumplir todos los requisitos a la vez. Entonces, no podemos crear pruebas que esperen una contraseña válida para cada caso, porque tendría que cumplir con el resto para que las pruebas sigan siendo válidas. Hacer esto sería repetir la misma prueba, que la contraseña sea válida.
Por este motivo, necesitamos utilizar el enfoque opuesto, probar que la contraseña no sea válida para cada caso utilizando contraseñas que no cumplan únicamente con ese caso, para asegurarnos de que estamos probando un caso aislado y si falla, es por una mala implementación.
Claramente, debe haber una prueba que espere una contraseña válida. Empezaremos por este último caso e iremos implementando el resto según se mencionan en el enunciado. ¿Por qué? Porque al ya tener una prueba para una contraseña válida, podemos enfocarnos en crear las pruebas e implementaciones como si fueran bloques para construir la solución completa que cubra todos los casos.
Caso: contraseña válida
Primero, vamos a crear dos archivos: password-validator.ts
y password-validator.test.ts
. El primero contendrá la función isStrongPassword
y el segundo, las pruebas.
// src/core/password-validator.ts
export function isStrongPassword(input: string) {
// implementación aquí
}
// src/tests/password-validator.test.ts
import {isStrongPassword} from "../core/password-validator";
Para agrupar nuestras pruebas, vamos a definir una suite que contenga Password Validator
como descripción.
// src/tests/password-validator.test.ts
describe("Password Validator", () => {
// pruebas aquí
});
Ahora, vamos a crear la prueba para el caso de una contraseña válida.
// src/tests/password-validator.test.ts
it('considers a password to be strong when all requirements are met', () => {
expect(isStrongPassword('StRonG_92bC')).toBe(true);
});
La implementación mínima sería simplemente regresar true
.
// src/core/password-validator.ts
export function isStrongPassword(input: string) {
return true;
}
No hay nada por refactorizar.
Caso: contraseña muy corta
Creamos la prueba:
// src/tests/password-validator.test.ts
it('fails when the password is too short', () => {
expect(isStrongPassword('abc')).toBe(false);
});
La implementación mínima sería regresar el resultado de evaluar si la longitud de la contraseña es mayor o igual a 6. Si lo es, entonces la contraseña es válida. De lo contrario, no lo es.
// src/core/password-validator.ts
export function isStrongPassword(password: string) {
return password.length >= 6;
}
No hay nada por refactorizar.
Caso: contraseña sin números
Creamos la prueba:
// src/tests/password-validator.test.ts
it('fails when the password is missing a number', () => {
expect(isStrongPassword('ABCdef_')).toBe(false);
});
La implementación mínima sería utilizar una expresión regular que busque números en la contraseña. Esta validación se anida a la anterior.
// src/core/password-validator.ts
export function isStrongPassword(password: string) {
return password.length >= 6 && /\d/g.test(password);
}
Vamos a usar este enfoque para agregar el resto de los criterios de validez. Anidar cada criterio como condición. Una contraseña válida será aquella que cumpla con todos. Ahora toca refactorizar. Como las expresiones regulares son difíciles de leer, siempre es buena idea extraerlas a su propia función, y ya que estamos, vamos a extraer también la validación de longitud, colocando la longitud mínima en una constante para facilitar su modificación en caso de que el criterio cambie.
// 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);
}
Caso: contraseña sin minúsculas
Creamos la prueba:
// src/tests/password-validator.test.ts
it('fails when the password is missing a lowercase', () => {
expect(isStrongPassword('ABCDEF_1')).toBe(false);
});
La implementación mínima sería usar una expresión regular que busque minúsculas en la contraseña.
// 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);
}
De igual manera, refactorizamos y extraemos esta nueva validación a su propia función.
// 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);
}
Caso: contraseña sin mayúsculas
Creamos la prueba:
// src/tests/password-validator.test.ts
it('fails when the password is missing an uppercase', () => {
expect(isStrongPassword('abcdef_1')).toBe(false);
});
La implementación mínima sería muy similar a la anterior, pero ahora de minúsculas, usamos mayúsculas.
// 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);
}
Vamos a refactorizar de la misma manera, extrayendo la nueva validación a su propia función. Además, como ya está quedando una línea muy larga, vamos a colocar cada validación en su propia línea para que la función sea más legible.
// 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);
}
Caso: contraseña sin guiones bajos
Llegamos al último caso. Siguiendo el mismo proceso, creamos la prueba:
// src/tests/password-validator.test.ts
it('fails when the password is missing an underscore', () => {
expect(isStrongPassword('Abcdef1')).toBe(false);
});
La implementación mínima sería utilizar una expresión regular que busque por guiones bajos en la contraseña, como en el caso de los números.
// 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);
}
Vamos a hacer la última refactorización y extraer la nueva validación a su propia función.
// 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);
}
¡Listo! Con esto quedaría lista nuestra función isStrongPassword
. Las pruebas nos dan la seguridad de que cubre todos los criterios especificados.
Enlace al repositorio de GitHub
Puedes encontrar esta kata, y el resto de ellas, aquí.
Conclusión
Al menos a mí me parece que, sin usar el enfoque de TDD, es más difícil llegar a una solución así de sencilla. Es perfectamente posible hacerlo, pero suele ser más difícil porque uno tiende a querer resolver el problema como un todo en lugar de dividirlo por partes y construir la solución de a poco.
Espero que este otro ejemplo de resolver problemas pensando en las pruebas primero te ayude a entender mejor la esencia de TDD. Ya sabes, si tienes alguna duda o quieres compartir algo, déjalo en los comentarios :)