Resolviendo la Kata NIF

En este kata vamos a orientarnos en una estrategia que aborde primero los sad paths, es decir, vamos a tratar primero los casos que provocarían un error. De este modo, primero desarrollaremos la validación de la estructura del input, para pasar después al del algoritmo.

Es habitual que las katas obvien temas como las validaciones, pero en este caso hemos preferido hacer un ejemplo que es más realista, en el sentido de que es una situación con la que lidiamos habitualmente. En el código de un proyecto en producción es fundamental la validación de datos de entrada y no está de más practicar con un ejercicio que pone su foco casi exclusivamente en ello.

Aparte, veremos un par de técnicas interesantes para transformar una interfaz pública sin romper los tests.

Enunciado de la kata

Crear una clase Nif, que será un Value Object para representar el Número de Identificación Fiscal de España. Es una combinación de una cadena de ocho caracteres numéricos, con una letra final que actúa como carácter de control.

Esta letra de control se obtiene calculando el resto de dividir la parte numérica del NIF entre 23 (mod 23). El resultado nos indica la fila en la que consultar la letra de control de la siguiente tabla. En esta tabla he incluido también ejemplos sencillos de NIF válidos para que los puedas usar en los tests.

Parte numérica Resto Letra Ejemplo NIF válido
00000023 0 T 00000023T
00000024 1 R 00000024R
00000025 2 W 00000025W
00000026 3 A 00000026A
00000027 4 G 00000027G
00000028 5 M 00000028M
00000029 6 Y 00000029Y
00000030 7 F 00000030F
00000031 8 P 00000031P
00000032 9 D 00000032D
00000033 10 X 00000033X
00000034 11 B 00000034B
00000035 12 N 00000035N
00000036 13 J 00000036J
00000037 14 Z 00000037Z
00000038 15 S 00000038S
00000039 16 Q 00000039Q
00000040 17 V 00000040V
00000041 18 H 00000041H
00000042 19 L 00000042L
00000043 20 C 00000043C
00000044 21 K 00000044K
00000045 22 E 00000045E

Puedes crear NIF inválidos simplemente escogiendo una parte numérica y la letra que no le corresponde.

Ejemplo inválido
00000000S
00000001M
00000002H
00000003Q
00000004E

Hay una excepción: los NIF para extranjeras (o NIE) pueden empezar por las letras X, Y o Z, que a los efectos de los cálculos se reemplazan por los números 0, 1 y 2, respectivamente. En ese caso X0000000T equivale a 00000000T.

Para evitar confusiones se han excluido las letras U, I, O y Ñ.

Una cadena que empieza por una letra distinta de X, Y, Z, o que contenga caracteres alfabéticos en las posiciones centrales también es inválida.

Lenguaje y enfoque

Esta kata la vamos a resolver en Go, por lo que vamos a matizar un poco su resultado. En este ejemplo vamos a crear un tipo de dato Nif, que será básicamente un string, y una función factoría NewNif que nos permitirá construir NIF validados a partir de un string que le pasamos.

Por otro lado, el testing en Go es también un poco particular. Aunque el lenguaje incorpora de forma estándar soporte para realizar tests, no incluye utilidades habituales como asserts.

Disclaimer

Para resolver esta kata me voy a aprovechar de la forma en que Go gestiona los errores. Estos se pueden devolver como una de las respuestas de una función, lo que te obliga a gestionarlos siempre de manera explícita.

Basar tests en mensajes de error no es una buena práctica, porque pueden cambiar con facilidad haciendo fallar los tests aunque no haya realmente una alteración de la funcionalidad. Sin embargo, en esta kata vamos a usar los mensajes de error como una especie de comodín temporal en el que apoyarnos haciendo que cambien de más específicos a más generales. Al acabar el ejercicio manejaremos únicamente dos errores posibles.

Creando la función constructora

En esta kata nos interesa empezar centrándonos en los sad paths, los casos en los que no vamos a poder usar el argumento pasado a la función constructora. De todas las innumerables combinaciones de string que la función podría recibir vamos primero a dar una respuesta a las que sabemos con seguridad que no nos van a servir porque no se ajustan a los requisitos. Esta respuesta será un error.

Empezaremos rechazando aquellas cadenas que sean demasiado largas, las que tienen más de nueve caracteres. Esto lo podemos describir con este test:

En el archivo nif/nif_test.go

1 package nif
2 
3 import "testing"
4 
5 func TestShouldFailWhenStringIsTooLong(t *testing.T) {
6 	NewNif()
7 }

De momento vamos a ignorar las respuestas de la función simplemente para forzarnos a implementar el mínimo de código.

Como es de suponer el test fallará porque no compila. Así que implementamos el código mínimo necesario, que puede ser tan pequeño como este:

Archivo nif/nif.go

1 package nif
2 
3 func NewNif() {
4 
5 }

Con esto ya logramos una base sobre la que construir.

Ahora podemos avanzar un paso más. La función debería aceptar un parámetro:

1 package nif
2 
3 import "testing"
4 
5 func TestShouldFailWhenStringIsTooLong(t *testing.T) {
6 	NewNif("01234567891011")
7 }

Volvemos a hacer pasar el test con:

1 package nif
2 
3 func NewNif(candidate string) {
4 
5 }

Y, finalmente, devolver:

  • el NIF cuando el que hemos pasado es válido.
  • un error en caso de que no se pueda.

En Go una función puede devolver varios valores y, por convención, se devuelven también los errores como último valor devuelto.

Esto proporciona una flexibilidad que no es habitual encontrar en otros lenguajes y nos deja jugar con algunas ideas que son como mínimo curiosas. Por ejemplo, nosotras vamos a ignorar de momento la respuesta de la función y centrarnos solo en los errores. Nuestro próximo test va a pedir que la función devuelva solo el error sin hacer nada con él. El if es, de momento, para que el compilador no proteste.

1 package nif
2 
3 import "testing"
4 
5 func TestShouldFailWhenStringIsTooLong(t *testing.T) {
6 	err := NewNif("01234567891011")
7 
8 	if err != nil {}
9 }

Este test nos indica que debemos devolver algo, así que por ahora indicamos que vamos a devolver un error, que puede ser nil.

1 package nif
2 
3 func NewNif(candidate string) error {
4 	return nil
5 }

Avancemos un paso más esperando que se produzca un error determinado cuando se cumple la circunstancia definida por el test, que la cadena es demasiado larga, y con lo que tendremos ya un primer test en condiciones:

 1 package nif
 2 
 3 import "testing"
 4 
 5 func TestShouldFailWhenStringIsTooLong(t *testing.T) {
 6 	err := NewNif("01234567891011")
 7 
 8 	if err.Error() != "too long" {
 9 		t.Errorf("Expected too long, got %s", err.Error())
10 	}
11 }

De nuevo fallará el test y para hacerlo pasar devolvemos incondicionalmente el error:

1 package nif
2 
3 import "errors"
4 
5 func NewNif(candidate string) error {
6 	return errors.New("too long")
7 }

Y con esto ya hemos hecho que nuestro primer test esté completo y pase. Podríamos ser un poco más estrictas en el manejo de la respuesta para contemplar el caso de que err sea nil, pero de momento es algo que no nos tiene que afectar.

En este punto me gustaría llamar tu atención al hecho de que no estamos resolviendo nada todavía: el error se devuelve de manera incondicional, por lo que estamos retrasando esta validación para más tarde.

Implementar la primera validación

Nuestro segundo test tiene como objetivo forzar la implementación de la validación que acabamos de posponer. Puede sonar un poco extraño, pero nos muestra que uno de los grandes beneficios de TDD es el hecho de poder posponer decisiones. Al hacerlo, tendremos un poco más de información, lo que siempre es una ventaja.

El test es muy similar al anterior:

 1 package nif
 2 
 3 import "testing"
 4 
 5 func TestShouldFailWhenStringIsTooLong(t *testing.T) {
 6 	err := NewNif("01234567891011")
 7 
 8 	if err.Error() != "too long" {
 9 		t.Errorf("Expected too long, got %s", err.Error())
10 	}
11 }
12 
13 func TestShouldFailWhenStringIsTooShort(t *testing.T) {
14 	err := NewNif("0123456")
15 
16 	if err.Error() != "too short" {
17 		t.Errorf("Expected too short, got %s", err.Error())
18 	}
19 }

Este test ya nos obliga a actuar de manera diferente en cada caso, así que vamos a implementar la validación que limita los string demasiado largos:

 1 package nif
 2 
 3 import "errors"
 4 
 5 func NewNif(candidate string) error {
 6 	if len(candidate) > 9 {
 7 		return errors.New("too long")
 8 	}
 9 
10 	return errors.New("too short")
11 }

Vuelvo a señalar que en este momento lo que dice el test no lo estamos implementando ahora. Lo haremos en el siguiente ciclo, pero el test se cumple al devolver el error esperado de forma incondicional.

No hay mucho más que podamos hacer en el código de producción, pero al fijarnos en los tests podemos ver que sería posible unificar un poco su estructura. Al fin y al cabo vamos a hacer una serie de ellos en los que pasamos un valor y esperamos un error determinado como respuesta.

Un test para dominarlos a todos

En Go existe una estructura de test similar a la que en otros lenguajes nos proporciona el uso de Data Providers: Table Test.

 1 package nif
 2 
 3 import "testing"
 4 
 5 func TestShouldFailIfCandidateIsInvalid(t *testing.T) {
 6 	tests := []struct {
 7 		name string
 8 		example string
 9 		expected string
10 	}{
11 		{"should fail if too long", "01234567891011", "too long"},
12 		{"should fail if too short", "01234", "too short"},
13 	}
14 	for _, test := range tests {
15 		t.Run(test.name, func(t *testing.T) {
16 			err := NewNif(test.example)
17 
18 			if err.Error() != test.expected {
19 				t.Errorf("Expected %s, got %s", test.expected, err.Error())
20 			}
21 		})
22 	}
23 }

Con esto conseguimos que ahora sea muy fácil y rápido añadir tests, sobre todo si son de la misma familia, como en este caso en el que pasamos cadenas candidatas inválidas y chequeamos por el error. Además, si hacemos cambios en la interfaz de la constructora solo tenemos un lugar en donde aplicarlos.

Con esto, tendríamos ya todo preparado para seguir desarrollando.

Completar la validación de la longitud y empezar a examinar la estructura

Con los dos tests anteriores verificamos si la cadena que vamos a examinar cumple la especificación de tener exactamente nueve caracteres, aunque de momento eso no está implementado. Lo haremos ahora.

Sin embargo, puede que te preguntes por qué no testear simplemente que la función rechaza los strings que no la cumplan, algo que podríamos hacer en un único test.

La razón es que en realidad hay dos posibles formas de que no se cumpla la especificación: el string tiene más de nueve caracteres o el string tiene menos. Si hacemos un solo test elegiremos uno de los dos casos, con lo cual no estamos garantizando que se cumpla el otro.

En este ejemplo concreto en que hay un único valor que nos interesa podríamos plantear la disyuntiva entre strings con longitud nueve y strings con longitud distinta de nueve. Sin embargo, es frecuente que tengamos que trabajar con intervalos de valores que, además, pueden estar abiertos o cerrados. En esa situación la estrategia de dos, o incluso más tests, es muchísimo más segura.

En cualquier caso, en el punto en el que estamos, para mover el desarrollo necesitamos añadir otro requisito en forma de test. Los dos tests existentes nos definen la longitud válida del string. El siguiente test pregunta por su estructura.

Y con el refactor que acabamos de hacer añadir un test es tremendamente sencillo.

Empezaremos por el principio. Los NIF válidos comienzan con un número, excepto un subconjunto de ellos que lo hace por alguna de las letras X, Y y Z. Una forma de definir el test es de la siguiente forma:

 1 package nif
 2 
 3 import "testing"
 4 
 5 func TestShouldFailIfCandidateIsInvalid(t *testing.T) {
 6 	tests := []struct {
 7 		name string
 8 		example string
 9 		expected string
10 	}{
11 		{
12 			"should fail if too long",
13 			"01234567891011", 
14 			"too long",
15 		},
16 		{
17 			"should fail if too short",
18 			"01234",
19 			"too short",
20 		},
21 		{
22 			"should fail if starts with a letter other than X, Y, Z", 
23 			"A12345678", 
24 			"bad start format",
25 		},
26 	}
27 	for _, test := range tests {
28 		t.Run(test.name, func(t *testing.T) {
29 			err := NewNif(test.example)
30 
31 			if err.Error() != test.expected {
32 				t.Errorf("Expected %s, got %s", test.expected, err.Error())
33 			}
34 		})
35 	}
36 }

Para hacer pasar el test, resolvemos primero el problema pendiente del anterior:

 1 package nif
 2 
 3 import "errors"
 4 
 5 func NewNif(candidate string) error {
 6 	if len(candidate) > 9 {
 7 		return errors.New("too long")
 8 	}
 9 
10 	if len(candidate) < 9 {
11 		return errors.New("too short")
12 	}
13 
14 	return errors.New("bad start format")
15 }

Aquí tenemos una oportunidad de refactor bastante clara que consistiría en unir las condicionales que evalúan la longitud del string. Sin embargo, esa va a hacer que el test falle, ya que al menos tendríamos que cambiar un mensaje de error.

La vía no muy limpia de cambiar a la vez test y código de producción

Una posibilidad es “saltarnos” temporalmente la condición de que el refactor sea con los tests en verde y hacer cambios a la vez en prod y test. Veamos qué pasa.

Lo primero sería cambiar el test para esperar un mensaje de error distinto, que será más genérico e igual para todos los casos que queremos consolidar en este paso:

 1 package nif
 2 
 3 import "testing"
 4 
 5 func TestShouldFailIfCandidateIsInvalid(t *testing.T) {
 6 	tests := []struct {
 7 		name string
 8 		example string
 9 		expected string
10 	}{
11 		{
12 			"should fail if too long",
13 			"01234567891011",
14 			"bad format",
15 		},
16 		{
17 			"should fail if too short", 
18 			"01234", 
19 			"bad format",
20 		},
21 		{
22 			"should fail if starts with a letter other than X, Y, Z",
23 			"A12345678", 
24 			"bad start format",
25 		},
26 	}
27 	for _, test := range tests {
28 		t.Run(test.name, func(t *testing.T) {
29 			err := NewNif(test.example)
30 
31 			if err.Error() != test.expected {
32 				t.Errorf("Expected %s, got %s", test.expected, err.Error())
33 			}
34 		})
35 	}
36 }

Esto hará fallar el test. Cosa que se puede arreglar cambiando el código de producción del mismo modo:

 1 package nif
 2 
 3 import "errors"
 4 
 5 func NewNif(candidate string) error {
 6 	if len(candidate) > 9 {
 7 		return errors.New("bad format")
 8 	}
 9 
10 	if len(candidate) < 9 {
11 		return errors.New("bad format")
12 	}
13 
14 	return errors.New("bad start format")
15 }

El test vuelve a pasar y estamos listas para el refactor. Pero no vamos a hacer eso.

La vía segura

Otra opción es hacer un refactor temporal en el test para hacerlo más tolerante. Simplemente, hacemos que sea posible devolver un error más genérico aparte del error específico.

 1 package nif
 2 
 3 import "testing"
 4 
 5 func TestShouldFailIfCandidateIsInvalid(t *testing.T) {
 6 	tests := []struct {
 7 		name     string
 8 		example  string
 9 		expected string
10 	}{
11 		{
12 			"should fail if too long",
13 			"01234567891011",
14 			"too long",
15 		},
16 		{
17 			"should fail if too short",
18 			"01234",
19 			"too short",
20 		},
21 		{
22 			"should fail if starts with a letter other than X, Y, Z",
23 			"A12345678",
24 			"bad start format",
25 		},
26 	}
27 	for _, test := range tests {
28 		t.Run(test.name, func(t *testing.T) {
29 			err := NewNif(test.example)
30 
31 			if err.Error() != test.expected && err.Error() != "bad format" {
32 				t.Errorf("Expected %s, got %s", test.expected, err.Error())
33 			}
34 		})
35 	}
36 }

Este cambio nos permite ahora hacer el cambio en producción sin romper nada:

 1 package nif
 2 
 3 import "errors"
 4 
 5 func NewNif(candidate string) error {
 6 	if len(candidate) > 9 {
 7 		return errors.New("bad format")
 8 	}
 9 
10 	if len(candidate) < 9 {
11 		return errors.New("bad format")
12 	}
13 
14 	return errors.New("bad end format")
15 }

El test sigue pasando y ahora sí podemos hacer el refactor.

Unificar la validación por longitud del string

Unificar las condicionales es fácil en este momento. Este es el primer paso, que incluyo aquí para tener un modelo de cómo hacer esto en caso de que fuese un intervalo de longitudes válidas.

 1 package nif
 2 
 3 import "errors"
 4 
 5 func NewNif(candidate string) error {
 6 	if len(candidate) > 9 || len(candidate) < 9 {
 7 		return errors.New("bad format")
 8 	}
 9 	
10 	return errors.New("bad start format")
11 }

Pero se puede hacer mejor:

 1 package nif
 2 
 3 import "errors"
 4 
 5 func NewNif(candidate string) error {
 6 	if len(candidate) != 9 {
 7 		return errors.New("bad format")
 8 	}
 9 
10 	return errors.New("bad start format")
11 }

Y un poco más expresivo:

 1 package nif
 2 
 3 import "errors"
 4 
 5 func NewNif(candidate string) error {
 6 	const maxlength = 9
 7 	
 8 	if len(candidate) != maxlength {
 9 		return errors.New("bad format")
10 	}
11 
12 	return errors.New("bad start format")
13 }

Finalmente, un nuevo refactor del test para contemplar estos cambios. Retiramos nuestro cambio temporal aunque es posible que tengamos que volver a utilizarlo en el futuro.

 1 package nif
 2 
 3 import "testing"
 4 
 5 func TestShouldFailIfCandidateIsInvalid(t *testing.T) {
 6 	tests := []struct {
 7 		name string
 8 		example string
 9 		expected string
10 	}{
11 		{
12 			"should fail if too long",
13 			"01234567891011",
14 			"bad format",
15 		},
16 		{
17 			"should fail if too short",
18 			"01234",
19 			"bad format",
20 		},
21 		{
22 			"should fail if starts with a letter other than X, Y, Z",
23 			"A12345678",
24 			"bad start format",
25 		},
26 	}
27 	for _, test := range tests {
28 		t.Run(test.name, func(t *testing.T) {
29 			err := NewNif(test.example)
30 
31 			if err.Error() != test.expected {
32 				t.Errorf("Expected %s, got %s", test.expected, err.Error())
33 			}
34 		})
35 	}
36 }

Fíjate que hemos podido hacer todos estos cambios sin que fallaran los tests.

Avanzando en la estructura

El código está bastante compacto, así que vamos a añadir un nuevo test que nos permita avanzar con la validez de la estructura. El fragmento central del NIF está compuesto solo por números, exactamente siete:

 1 package nif
 2 
 3 import "testing"
 4 
 5 func TestShouldFailIfCandidateIsInvalid(t *testing.T) {
 6 	tests := []struct {
 7 		name string
 8 		example string
 9 		expected string
10 	}{
11 		{
12 			"should fail if too long",
13 			"01234567891011",
14 			"bad format",
15 		},
16 		{
17 			"should fail if too short",
18 			"01234",
19 			"bad format",
20 		},
21 		{
22 			"should fail if starts with a letter other than X, Y, Z",
23 			"A12345678",
24 			"bad start format",
25 		},
26 		{
27 			"should fail if doesn't have 7 digit in the middle",
28 			"0123X567R",
29 			"bad middle format",
30 		},
31 	}
32 	for _, test := range tests {
33 		t.Run(test.name, func(t *testing.T) {
34 			err := NewNif(test.example)
35 
36 			if err.Error() != test.expected {
37 				t.Errorf("Expected %s, got %s", test.expected, err.Error())
38 			}
39 		})
40 	}
41 }

Lo lanzamos para asegurarnos de que falla por la razón adecuada. Para hacer pasar el test tenemos que resolver primero el test anterior, por lo que añadiremos código para verificar que el primer símbolo es un número o una letra en el conjunto X, Y y Z. Lo haremos con una expresión regular:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 )
 7 
 8 func NewNif(candidate string) error {
 9 	const maxlength = 9
10 
11 	if len(candidate) != maxlength {
12 		return errors.New("bad format")
13 	}
14 
15 	invalid := regexp.MustCompile(`(?i)^[^0-9XYZ].*`)
16 
17 	if invalid.MatchString(candidate) {
18 		return errors.New("bad start format")
19 	}
20 	
21 	return errors.New("bad middle format")
22 }

Con este código hacemos pasar el test, pero vamos a hacer un refactor.

Invertir la condicional

Tiene sentido que en vez de hacer match contra una expresión regular que excluya los string no válidos, hagamos match contra una expresión que los detecte. Con eso tendremos que invertir la condicional. Lo cierto es que el cambio es bastante pequeño:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 )
 7 
 8 func NewNif(candidate string) error {
 9 	const maxlength = 9
10 
11 	if len(candidate) != maxlength {
12 		return errors.New("bad format")
13 	}
14 
15 	valid := regexp.MustCompile(`(?i)^[0-9XYZ].*`)
16 
17 	if !valid.MatchString(candidate) {
18 		return errors.New("bad start format")
19 	}
20 	
21 	return errors.New("bad middle format")
22 }

El final de la estructura

Nos estamos acercando al final de la validación estructural del NIF, necesitamos un test que nos diga cuáles rechazar en función de su último símbolo, lo que nos llevará a resolver el problema que quedaba pendiente del test anterior:

 1 package nif
 2 
 3 import "testing"
 4 
 5 func TestShouldFailIfCandidateIsInvalid(t *testing.T) {
 6 	tests := []struct {
 7 		name string
 8 		example string
 9 		expected string
10 	}{
11 		{
12 			"should fail if too long",
13 			"01234567891011",
14 			"bad format",
15 		},
16 		{
17 			"should fail if too short",
18 			"01234",
19 			"bad format",
20 		},
21 		{
22 			"should fail if starts with a letter other than X, Y, Z",
23 			"A12345678",
24 			"bad start format",
25 		},
26 		{
27 			"should fail if doesn't have 7 digit in the middle",
28 			"0123X567R",
29 			"bad middle format",
30 		},
31 		{
32 			"should fail if doesn't end with a valid control letter",
33 			"01234567U",
34 			"invalid end format",
35 		},
36 	}
37 	for _, test := range tests {
38 		t.Run(test.name, func(t *testing.T) {
39 			err := NewNif(test.example)
40 
41 			if err.Error() != test.expected {
42 				t.Errorf("Expected %s, got %s", test.expected, err.Error())
43 			}
44 		})
45 	}
46 }

De las cuatro letras no válidas tomamos la U como ejemplo, pero podrían ser: la I, la Ñ y la O.

Sin embargo, para hacer pasar el test, lo que hacemos es asegurarnos que el anterior se va a cumplir. Lo más fácil es implementar eso de forma separada:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 )
 7 
 8 func NewNif(candidate string) error {
 9 	const maxlength = 9
10 
11 	if len(candidate) != maxlength {
12 		return errors.New("bad format")
13 	}
14 
15 	valid := regexp.MustCompile(`(?i)^[0-9XYZ].*`)
16 
17 	if !valid.MatchString(candidate) {
18 		return errors.New("bad start format")
19 	}
20 
21 	valid = regexp.MustCompile(`(?i)^.\d{7}.*`)
22 
23 	if !valid.MatchString(candidate) {
24 		return errors.New("bad middle format")
25 	}
26 
27 	return errors.New("invalid end format")
28 }

Compactando el algoritmo

Con esto pasa el test y nos encontramos con una situación familiar que ya hemos resuelto antes: tenemos que convertir los errores en más genéricos con la ayuda temporal de un control extra en el test:

 1 package nif
 2 
 3 import "testing"
 4 
 5 func TestShouldFailIfCandidateIsInvalid(t *testing.T) {
 6 	tests := []struct {
 7 		name string
 8 		example string
 9 		expected string
10 	}{
11 		{
12 			"should fail if too long",
13 			"01234567891011",
14 			"bad format",
15 		},
16 		{
17 			"should fail if too short",
18 			"01234",
19 			"bad format",
20 		},
21 		{
22 			"should fail if starts with a letter other than X, Y, Z",
23 			"A12345678",
24 			"bad start format",
25 		},
26 		{
27 			"should fail if doesn't have 7 digit in the middle",
28 			"0123X567R",
29 			"bad middle format",
30 		},
31 		{
32 			"should fail if doesn't end with a valid control letter",
33 			"01234567U",
34 			"invalid end format",
35 		},
36 	}
37 	for _, test := range tests {
38 		t.Run(test.name, func(t *testing.T) {
39 			err := NewNif(test.example)
40 
41 			if err.Error() != test.expected && err.Error() != "bad format" {
42 				t.Errorf("Expected %s, got %s", test.expected, err.Error())
43 			}
44 		})
45 	}
46 }

Hacemos el cambio en los mensajes de error en el código de producción:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 )
 7 
 8 func NewNif(candidate string) error {
 9 	const maxlength = 9
10 
11 	if len(candidate) != maxlength {
12 		return errors.New("bad format")
13 	}
14 
15 	valid := regexp.MustCompile(`(?i)^[0-9XYZ].*`)
16 
17 	if !valid.MatchString(candidate) {
18 		return errors.New("bad format")
19 	}
20 
21 	valid = regexp.MustCompile(`(?i)^.\d{7}.*`)
22 
23 	if !valid.MatchString(candidate) {
24 		return errors.New("bad format")
25 	}
26 
27 	return errors.New("invalid end format")
28 }

Ahora unificamos la expresión regular y las condicionales:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 )
 7 
 8 func NewNif(candidate string) error {
 9 	const maxlength = 9
10 
11 	if len(candidate) != maxlength {
12 		return errors.New("bad format")
13 	}
14 
15 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}.*`)
16 
17 	if !valid.MatchString(candidate) {
18 		return errors.New("bad format")
19 	}
20 	
21 	return errors.New("invalid end format")
22 }

Todavía podemos hacer un cambio pequeño pero importante. La última parte de la expresión regular .* está para cumplir el requisito de que se haga match de toda la cadena, pero realmente no necesitamos el cuantificador, ya que nos basta un carácter:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 )
 7 
 8 func NewNif(candidate string) error {
 9 	const maxlength = 9
10 
11 	if len(candidate) != maxlength {
12 		return errors.New("bad format")
13 	}
14 
15 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}.$`)
16 
17 	if !valid.MatchString(candidate) {
18 		return errors.New("bad format")
19 	}
20 
21 	return errors.New("invalid end format")
22 }

Y esto nos revela un detalle, la expresión regular hace match únicamente en cadenas de exactamente nueve caracteres, por lo que la validación inicial de la longitud resulta innecesaria:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 )
 7 
 8 func NewNif(candidate string) error {
 9 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}.$`)
10 
11 	if !valid.MatchString(candidate) {
12 		return errors.New("bad format")
13 	}
14 
15 	return errors.New("invalid end format")
16 }

Tanto camino recorrido para desandarlo. Sin embargo, al principio no sabíamos esto y es ahí donde está el valor del proceso.

Por último, cambiamos el test para reflejar los cambios y volver a quitar nuestro apoyo temporal:

 1 package nif
 2 
 3 import "testing"
 4 
 5 func TestShouldFailIfCandidateIsInvalid(t *testing.T) {
 6 	tests := []struct {
 7 		name string
 8 		example string
 9 		expected string
10 	}{
11 		{
12 			"should fail if too long",
13 			"01234567891011",
14 			"bad format",
15 		},
16 		{
17 			"should fail if too short",
18 			"01234",
19 			"bad format",
20 		},
21 		{
22 			"should fail if starts with a letter other than X, Y, Z",
23 			"A12345678",
24 			"bad format",
25 		},
26 		{
27 			"should fail if doesn't have 7 digit in the middle",
28 			"0123X567R",
29 			"bad format",
30 		},
31 		{
32 			"should fail if doesn't end with a valid control letter",
33 			"01234567U",
34 			"invalid end format",
35 		},
36 	}
37 	for _, test := range tests {
38 		t.Run(test.name, func(t *testing.T) {
39 			err := NewNif(test.example)
40 
41 			if err.Error() != test.expected {
42 				t.Errorf("Expected %s, got %s", test.expected, err.Error())
43 			}
44 		})
45 	}
46 }

Terminando la validación estructural

Necesitamos un nuevo test para terminar la parte de la validación estructural. Los tests existentes nos garantizarían la corrección de los strings, pero la siguiente validación ya implica el algoritmo para calcular la letra de control.

El test que necesitamos ahora controla que no podemos usar un NIF estructuralmente válido, pero en el que la letra de control sea incorrecta. Al enunciar la kata pusimos algunos ejemplos, como 00000000S. Este es el test:

 1 package nif
 2 
 3 import "testing"
 4 
 5 func TestShouldFailIfCandidateIsInvalid(t *testing.T) {
 6 	tests := []struct {
 7 		name string
 8 		example string
 9 		expected string
10 	}{
11 		{
12 			"should fail if too long",
13 			"01234567891011",
14 			"bad format",
15 		},
16 		{
17 			"should fail if too short",
18 			"01234",
19 			"bad format",
20 		},
21 		{
22 			"should fail if starts with a letter other than X, Y, Z",
23 			"A12345678",
24 			"bad format",
25 		},
26 		{
27 			"should fail if doesn't have 7 digit in the middle",
28 			"0123X567R",
29 			"bad format",
30 		},
31 		{
32 			"should fail if doesn't end with a valid control letter",
33 			"01234567U",
34 			"invalid end format",
35 		},
36 		{
37 			"should fail if doesn't end with the right control letter",
38 			"00000000S",
39 			"bad control letter",
40 		},
41 
42 	}
43 	for _, test := range tests {
44 		t.Run(test.name, func(t *testing.T) {
45 			err := NewNif(test.example)
46 
47 			if err.Error() != test.expected {
48 				t.Errorf("Expected %s, got %s", test.expected, err.Error())
49 			}
50 		})
51 	}
52 }

Y he aquí el código que lo hace pasar:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 )
 7 
 8 func NewNif(candidate string) error {
 9 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}.$`)
10 
11 	if !valid.MatchString(candidate) {
12 		return errors.New("bad format")
13 	}
14 
15 	valid = regexp.MustCompile(`(?i).*[^UIOÑ0-9]$`)
16 
17 	if !valid.MatchString(candidate) {
18 		return errors.New("invalid end format")
19 	}
20 	
21 	return errors.New("bad control letter")
22 }

Y, cómo no, toca refactorizar.

Compactando la validación

Este refactor es bastante obvio, pero tenemos que volver a proteger el test temporalmente:

 1 package nif
 2 
 3 import "testing"
 4 
 5 func TestShouldFailIfCandidateIsInvalid(t *testing.T) {
 6 	tests := []struct {
 7 		name string
 8 		example string
 9 		expected string
10 	}{
11 		{
12 			"should fail if too long",
13 			"01234567891011",
14 			"bad format",
15 		},
16 		{
17 			"should fail if too short",
18 			"01234",
19 			"bad format",
20 		},
21 		{
22 			"should fail if starts with a letter other than X, Y, Z",
23 			"A12345678",
24 			"bad format",
25 		},
26 		{
27 			"should fail if doesn't have 7 digit in the middle",
28 			"0123X567R",
29 			"bad format",
30 		},
31 		{
32 			"should fail if doesn't end with a valid control letter",
33 			"01234567U",
34 			"invalid end format",
35 		},
36 		{
37 			"should fail if doesn't end with the right control letter",
38 			"00000000S",
39 			"bad control letter",
40 		},
41 	}
42 	for _, test := range tests {
43 		t.Run(test.name, func(t *testing.T) {
44 			err := NewNif(test.example)
45 
46 			if err.Error() != test.expected  && err.Error() != "bad format" {
47 				t.Errorf("Expected %s, got %s", test.expected, err.Error())
48 			}
49 		})
50 	}
51 }

Hacemos más general el error para poder unificar las expresiones regulares y las condicionales:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 )
 7 
 8 func NewNif(candidate string) error {
 9 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}.$`)
10 
11 	if !valid.MatchString(candidate) {
12 		return errors.New("bad format")
13 	}
14 
15 	valid = regexp.MustCompile(`(?i).*[^UIOÑ0-9]$`)
16 
17 	if !valid.MatchString(candidate) {
18 		return errors.New("bad format")
19 	}
20 
21 	return errors.New("bad control letter")
22 }

Y hacemos ahora la unificación, mientras los tests siguen pasando:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 )
 7 
 8 func NewNif(candidate string) error {
 9 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
10 
11 	if !valid.MatchString(candidate) {
12 		return errors.New("bad format")
13 	}
14 
15 	return errors.New("bad control letter")
16 }

Con esto terminamos la validación de la estructura y nos quedaría implementar el algoritmo mod23. Pero para eso necesitamos un pequeño cambio de enfoque.

Seamos optimistas

El algoritmo es, de hecho, muy sencillo: obtener un resto (de dividir por 23) y buscar la posición indicada por el resultado en una tabla. Es fácil de implementar en una sola iteración. Sin embargo, vamos a hacerlo más lentamente.

Hasta ahora nuestros tests eran pesimistas porque esperaban ejemplos incorrectos de NIF para poder pasar. Nuestros tests ahora tienen que ser optimistas, es decir, van a esperar que le pasemos ejemplos de NIF válidos.

En este punto introduciremos un cambio. Si recuerdas, de momento solo estamos devolviendo el error y la interfaz final de la función devolverá el string validado como un tipo NIF que vamos a crear para la ocasión.

Es decir, tenemos que cambiar el código para que devuelva algo, y que ese algo sea de un tipo que todavía no existe.

Para hacer este cambio sin romper los tests vamos a hacer una técnica de refactor un tanto rebuscada.

Cambiando la interfaz pública

En primer lugar, extraemos el cuerpo de NewNif a otra función:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 )
 7 
 8 func NewNif(candidate string) error {
 9 	return FullNewNif(candidate)
10 }
11 
12 func FullNewNif(candidate string) error {
13 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
14 
15 	if !valid.MatchString(candidate) {
16 		return errors.New("bad format")
17 	}
18 
19 	return errors.New("bad control letter")
20 }

Los tests siguen pasando. Ahora introducimos una variable:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 )
 7 
 8 func NewNif(candidate string) error {
 9 	err := FullNewNif(candidate)
10 	return err
11 }
12 
13 func FullNewNif(candidate string) error {
14 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
15 
16 	if !valid.MatchString(candidate) {
17 		return errors.New("bad format")
18 	}
19 
20 	return errors.New("bad control letter")
21 }

Con esto podemos hacer que FullNewNif devuelva el string sin afectar al test porque queda encapsulado dentro de NewNif.

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 )
 7 
 8 func NewNif(candidate string) error {
 9 	_, err := FullNewNif(candidate)
10 	return err
11 }
12 
13 func FullNewNif(candidate string) (string, error) {
14 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
15 
16 	if !valid.MatchString(candidate) {
17 		return "", errors.New("bad format")
18 	}
19 
20 	return candidate, errors.New("bad control letter")
21 }

Los tests siguen pasando y casi hemos acabado. En el test cambiamos el uso de NewNif por FullNewNif.

 1 package nif
 2 
 3 import "testing"
 4 
 5 func TestShouldFailIfCandidateIsInvalid(t *testing.T) {
 6 	tests := []struct {
 7 		name string
 8 		example string
 9 		expected string
10 	}{
11 		{
12 			"should fail if too long",
13 			"01234567891011",
14 			"bad format",
15 		},
16 		{
17 			"should fail if too short",
18 			"01234",
19 			"bad format",
20 		},
21 		{
22 			"should fail if starts with a letter other than X, Y, Z",
23 			"A12345678",
24 			"bad format",
25 		},
26 		{
27 			"should fail if doesn't have 7 digit in the middle",
28 			"0123X567R",
29 			"bad format",
30 		},
31 		{
32 			"should fail if doesn't end with a valid control letter",
33 			"01234567U",
34 			"invalid end format",
35 		},
36 		{
37 			"should fail if doesn't end with the right control letter",
38 			"00000000S",
39 			"bad control letter",
40 		},
41 	}
42 	for _, test := range tests {
43 		t.Run(test.name, func(t *testing.T) {
44 			_, err := FullNewNif(test.example)
45 
46 			if err.Error() != test.expected  && err.Error() != "bad format" {
47 				t.Errorf("Expected %s, got %s", test.expected, err.Error())
48 			}
49 		})
50 	}
51 }

Siguen pasando los tests. Ahora la función devuelve los dos parámetros que queríamos y no hemos roto los tests. Podemos eliminar la función NewNif original.

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 )
 7 
 8 func FullNewNif(candidate string) (string, error) {
 9 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
10 
11 	if !valid.MatchString(candidate) {
12 		return "", errors.New("bad format")
13 	}
14 
15 	return candidate, errors.New("bad control letter")
16 }

Y usar las herramientas del IDE para cambiar el nombre de la función FullNewNif a NewNif.

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 )
 7 
 8 func NewNif(candidate string) (string, error) {
 9 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
10 
11 	if !valid.MatchString(candidate) {
12 		return "", errors.New("bad format")
13 	}
14 
15 	return candidate, errors.New("bad control letter")
16 }

Ahora sí

Nuestro objetivo ahora es empujar la implementación del algoritmo mod23. Esta vez los tests esperan que la cadena sea válida. Además, queremos forzar que se devuelva el tipo Nif en lugar de string.

 1 func TestShouldCreateNifTypeWithValidCandidate(t *testing.T) {
 2 	tests := []struct {
 3 		name string
 4 		example string
 5 	}{
 6 		{"should accept mod23 being 0", "00000023T"},
 7 	}
 8 	for _, test := range tests {
 9 		t.Run(test.name, func(t *testing.T) {
10 			nif, err := NewNif(test.example)
11 			
12 			if nif != Nif(test.example) {
13 				t.Errorf("Expected Nif(%s), got %s", test.example, nif)
14 			}
15 			
16 			if err != nil {
17 				t.Errorf("Unexpected error %s", err.Error())
18 			}
19 		})
20 	}
21 }

En un primer paso cambiamos el código de producción para introducir y usar el tipo Nif:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 )
 7 
 8 type Nif string
 9 
10 func NewNif(candidate string) (Nif, error) {
11 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
12 
13 	if !valid.MatchString(candidate) {
14 		return "", errors.New("bad format")
15 	}
16 
17 	return "", errors.New("bad control letter")
18 }

Ahora el test estará fallando porque no hemos validado nada todavía. Para hacerlo pasar añadimos un condicional:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 )
 7 
 8 type Nif string
 9 
10 func NewNif(candidate string) (Nif, error) {
11 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
12 
13 	if !valid.MatchString(candidate) {
14 		return "", errors.New("bad format")
15 	}
16 
17 	if candidate == "00000023T" {
18 		return Nif(candidate), nil
19 	}
20 
21 	return "", errors.New("bad control letter")
22 }

Una nota sobre Go es que los tipos custom no pueden tener valor nil, sino vacío. Por eso en caso de error devolvemos string vacío.

Avanzando el algoritmo

De momento no hay muchos motivos para hacer refactor, así que vamos a introducir un test que nos ayude a avanzar un poco. En principio, queremos lograr que nos impulse a separar la parte numérica de la letra de control.

Una posibilidad sería testear otro NIF que acabe con la letra T, como el 00000046T.

 1 func TestShouldCreateNifTypeWithValidCandidate(t *testing.T) {
 2 	tests := []struct {
 3 		name string
 4 		example string
 5 	}{
 6 		{"should accept mod23 being 0", "00000023T"},
 7 		{"should accept mod23 being 0 letter T", "00000046T"},
 8 	}
 9 	for _, test := range tests {
10 		t.Run(test.name, func(t *testing.T) {
11 			nif, err := NewNif(test.example)
12 
13 			if nif != Nif(test.example) {
14 				t.Errorf("Expected Nif(%s), got %s", test.example, nif)
15 			}
16 
17 			if err != nil {
18 				t.Errorf("Unexpected error %s", err.Error())
19 			}
20 		})
21 	}
22 }

Para hacer pasar el test, podemos hacer esta implementación sencilla:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 )
 7 
 8 type Nif string
 9 
10 func NewNif(candidate string) (Nif, error) {
11 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
12 
13 	if !valid.MatchString(candidate) {
14 		return "", errors.New("bad format")
15 	}
16 
17 	if candidate == "00000023T" {
18 		return Nif(candidate), nil
19 	}
20 
21 	if candidate == "00000046T" {
22 		return Nif(candidate), nil
23 	}
24 
25 	return "", errors.New("bad control letter")
26 }

Y ahora empezamos a refactorizar.

Más refactor

En el código de producción podemos ver lo que hay de diferente y de común entre los ejemplos. En ambos la letra de control es T y la parte numérica es divisible entre 23, por lo que su mod23 será 0.

Ahora podemos hacer el refactor. Un primer paso.

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 )
 7 
 8 type Nif string
 9 
10 func NewNif(candidate string) (Nif, error) {
11 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
12 
13 	if !valid.MatchString(candidate) {
14 		return "", errors.New("bad format")
15 	}
16 
17 	control := string(candidate[8])
18 
19 	if control == "T" {
20 		return Nif(candidate), nil
21 	}
22 
23 	return "", errors.New("bad control letter")
24 }

Y, después de ver pasar los tests, el segundo paso:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 	"strconv"
 7 )
 8 
 9 type Nif string
10 
11 func NewNif(candidate string) (Nif, error) {
12 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
13 
14 	if !valid.MatchString(candidate) {
15 		return "", errors.New("bad format")
16 	}
17 
18 	control := string(candidate[8])
19 
20 	numeric, _ := strconv.Atoi(candidate[0:8])
21 
22 	modulus := numeric % 23
23 	
24 	if control == "T" && modulus == 0 {
25 		return Nif(candidate), nil
26 	}
27 
28 	return "", errors.New("bad control letter")
29 }

Con este cambio los tests pasan y acepta todos los NIF válidos acabados en T.

Validando más letras de control

En este tipo de algoritmos no tiene mucho sentido intentar validar todas las letras de control, pero podemos introducir una más para forzarnos a entender cómo debería evolucionar el código ahora. Probaremos con una nueva letra:

 1 package nif
 2 
 3 import "testing"
 4 
 5 func TestShouldFailIfCandidateIsInvalid(t *testing.T) {
 6 	tests := []struct {
 7 		name string
 8 		example string
 9 		expected string
10 	}{
11 		{
12 			"should fail if too long",
13 			"01234567891011",
14 			"bad format",
15 		},
16 		{
17 			"should fail if too short",
18 			"01234",
19 			"bad format",
20 		},
21 		{
22 			"should fail if starts with a letter other than X, Y, Z",
23 			"A12345678",
24 			"bad format",
25 		},
26 		{
27 			"should fail if doesn't have 7 digit in the middle",
28 			"0123X567R",
29 			"bad format",
30 		},
31 		{
32 			"should fail if doesn't end with a valid control letter",
33 			"01234567U",
34 			"invalid end format",
35 		},
36 		{
37 			"should fail if doesn't end with the right control letter",
38 			"00000000S",
39 			"bad control letter",
40 		},
41 	}
42 	for _, test := range tests {
43 		t.Run(test.name, func(t *testing.T) {
44 			_, err := NewNif(test.example)
45 
46 			if err.Error() != test.expected  && err.Error() != "bad format" {
47 				t.Errorf("Expected %s, got %s", test.expected, err.Error())
48 			}
49 		})
50 	}
51 }
52 
53 func TestShouldCreateNifTypeWithValidCandidate(t *testing.T) {
54 	tests := []struct {
55 		name string
56 		example string
57 	}{
58 		{"should accept mod23 being 0", "00000000T"},
59 		{"should accept mod23 being 0 letter T", "00000023T"},
60 		{"should accept mod23 being 1 letter R", "00000024R"},
61 	}
62 	for _, test := range tests {
63 		t.Run(test.name, func(t *testing.T) {
64 			nif, err := NewNif(test.example)
65 
66 			if nif != Nif(test.example) {
67 				t.Errorf("Expected Nif(%s), got %s", test.example, nif)
68 			}
69 
70 			if err != nil {
71 				t.Errorf("Unexpected error %s", err.Error())
72 			}
73 		})
74 	}
75 }

Ya tenemos este test fallando, así que vamos a hacer una implementación muy sencilla:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 	"strconv"
 7 )
 8 
 9 type Nif string
10 
11 func NewNif(candidate string) (Nif, error) {
12 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
13 
14 	if !valid.MatchString(candidate) {
15 		return "", errors.New("bad format")
16 	}
17 
18 	control := string(candidate[8])
19 
20 	numeric, _ := strconv.Atoi(candidate[0:8])
21 
22 	modulus := numeric % 23
23 
24 	if control == "T" && modulus == 0 {
25 		return Nif(candidate), nil
26 	}
27 
28 	if control == "R" && modulus == 1 {
29 		return Nif(candidate), nil
30 	}
31 
32 	return "", errors.New("bad control letter")
33 }

Esto ya nos da una idea de por dónde van los tiros: un mapa entre letras de control y el resto al dividir por 23. Sin embargo, es frecuente que los strings puedan funcionar como arrays en muchos lenguajes, por lo que nos basta tener un string con todas las letras de control ordenadas y acceder a la letra en la posición indicada por el módulo para saber cuál es la correcta.

Un refactor para más simplicidad

Primero implementamos una versión simple de esta idea:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 	"strconv"
 7 )
 8 
 9 type Nif string
10 
11 func NewNif(candidate string) (Nif, error) {
12 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
13 
14 	if !valid.MatchString(candidate) {
15 		return "", errors.New("bad format")
16 	}
17 
18 	controlMap := "TR"
19 
20 	control := candidate[8]
21 
22 	numeric, _ := strconv.Atoi(candidate[0:8])
23 
24 	modulus := numeric % 23
25 
26 	if control == controlMap[modulus] {
27 		return Nif(candidate), nil
28 	}
29 
30 	return "", errors.New("bad control letter")
31 }

Ya tenemos una primera versión. Luego añadiremos la lista completa de letras, pero podemos intentar arreglar un poco el código actual. Primero hacemos que controlMap sea constante:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 	"strconv"
 7 )
 8 
 9 type Nif string
10 
11 func NewNif(candidate string) (Nif, error) {
12 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
13 
14 	if !valid.MatchString(candidate) {
15 		return "", errors.New("bad format")
16 	}
17 
18 	const controlMap = "TR"
19 
20 	control := candidate[8]
21 
22 	numeric, _ := strconv.Atoi(candidate[0:8])
23 
24 	modulus := numeric % 23
25 
26 	if control == controlMap[modulus] {
27 		return Nif(candidate), nil
28 	}
29 
30 	return "", errors.New("bad control letter")
31 }

En realidad podríamos extraer toda la parte del cálculo del módulo a otra función. Primero reorganizamos el código para controlar mejor la extracción:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 	"strconv"
 7 )
 8 
 9 type Nif string
10 
11 func NewNif(candidate string) (Nif, error) {
12 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
13 
14 	if !valid.MatchString(candidate) {
15 		return "", errors.New("bad format")
16 	}
17 
18 	control := candidate[8]
19 	
20 	const controlMap = "TR"
21 	numeric, _ := strconv.Atoi(candidate[0:8])
22 	modulus := numeric % 23
23 	shouldBe := controlMap[modulus]
24 	
25 	if control == shouldBe {
26 		return Nif(candidate), nil
27 	}
28 
29 	return "", errors.New("bad control letter")
30 }

Recuerda verificar que pasan los tests. Ahora extraemos la función:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 	"strconv"
 7 )
 8 
 9 type Nif string
10 
11 func NewNif(candidate string) (Nif, error) {
12 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
13 
14 	if !valid.MatchString(candidate) {
15 		return "", errors.New("bad format")
16 	}
17 
18 	control := candidate[8]
19 
20 	if control == shouldHaveControl(candidate) {
21 		return Nif(candidate), nil
22 	}
23 
24 	return "", errors.New("bad control letter")
25 }
26 
27 func shouldHaveControl(candidate string) uint8 {
28 	const controlMap = "TR"
29 	numeric, _ := strconv.Atoi(candidate[0:8])
30 	modulus := numeric % 23
31 	
32 	return controlMap[modulus]
33 }

Y podemos compactar el código un poco más, mientras que añadimos las demás letras de control. A primera vista parece “trampa”, pero en el fondo no es más que generalizar un algoritmo que se podría enunciar como “toma la letra que hay en la posición dada por el mod23 de la parte numérica”.

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 	"strconv"
 7 )
 8 
 9 type Nif string
10 
11 func NewNif(candidate string) (Nif, error) {
12 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
13 
14 	if !valid.MatchString(candidate) {
15 		return "", errors.New("bad format")
16 	}
17 
18 	if candidate[8] == shouldHaveControl(candidate) {
19 		return Nif(candidate), nil
20 	}
21 
22 	return "", errors.New("bad control letter")
23 }
24 
25 func shouldHaveControl(candidate string) uint8 {
26 	const controlMap = "TRWAGMYFPDXBNJZSQVHLCKE"
27 
28 	numeric, _ := strconv.Atoi(candidate[0:8])
29 	modulus := numeric % 23
30 
31 	return controlMap[modulus]
32 }

Con esto ya podemos validar todos los NIF, excepto los NIE, que empiezan por las letras X, Y o Z.

Dar soporte a NIE

Ahora que hemos implementado el algoritmo general vamos a tratar sus excepciones, que no son tanto. Los NIE comienzan con una letra que a efectos del cálculo se reemplaza con un número.

El test que parece más evidente en este punto es el siguiente:

 1 func TestShouldCreateNifTypeWithValidCandidate(t *testing.T) {
 2 	tests := []struct {
 3 		name string
 4 		example string
 5 	}{
 6 		{"should accept mod23 being 0", "00000000T"},
 7 		{"should accept mod23 being 0 letter T", "00000023T"},
 8 		{"should accept mod23 being 1 letter R", "00000024R"},
 9 		{"should accept NIE starting with X", "X0000023T"},
10 	}
11 	for _, test := range tests {
12 		t.Run(test.name, func(t *testing.T) {
13 			nif, err := NewNif(test.example)
14 
15 			if nif != Nif(test.example) {
16 				t.Errorf("Expected Nif(%s), got %s", test.example, nif)
17 			}
18 
19 			if err != nil {
20 				t.Errorf("Unexpected error %s", err.Error())
21 			}
22 		})
23 	}
24 }

El caso X0000023T es equivalente a 00000023T, ¿afectará eso al resultado del test?

Ejecutamos el test y… ¿Sorpresa? El test pasa. Esto ocurre porque la conversión que hacemos en esta línea genera un error que actualmente estamos ignorando, pero permite que la parte numérica siga siendo equivalente a 23, cuyo mod23 es 0 y le corresponde igualmente la letra T.

1 	numeric, _ := strconv.Atoi(candidate[0:8])

En otros lenguajes la conversión no falla, pero asume la X como 0 al realizar la conversión.

En cualquier caso eso nos abre dos posibles caminos:

  • anular este test y refactorizar el código de producción para tratar el error y que el test falle cuando lo volvamos a incluir
  • probar otro ejemplo que sí pueda fallar (Y0000000Z) y hacer el cambio después

Posiblemente, para este caso la segunda opción sería más que suficiente, ya que con nuestras validaciones estructurales ya garantizaríamos que el error no tiene posibilidad de aparecer una vez que la función esté completamente desarrollada.

Sin embargo, podría ser interesante introducir la gestión del error. Manejar los errores, incluyendo los que nunca podrían llegar a pasar, es siempre una buena práctica.

Así que, anulemos el test e introduzcamos un refactor para manejar el error:

 1 func TestShouldCreateNifTypeWithValidCandidate(t *testing.T) {
 2 	tests := []struct {
 3 		name string
 4 		example string
 5 	}{
 6 		{"should accept mod23 being 0", "00000000T"},
 7 		{"should accept mod23 being 0 letter T", "00000023T"},
 8 		{"should accept mod23 being 1 letter R", "00000024R"},
 9 		//{"should accept NIE starting with X", "X0000023X"},
10 	}
11 	for _, test := range tests {
12 		t.Run(test.name, func(t *testing.T) {
13 			nif, err := NewNif(test.example)
14 
15 			if nif != Nif(test.example) {
16 				t.Errorf("Expected Nif(%s), got %s", test.example, nif)
17 			}
18 
19 			if err != nil {
20 				t.Errorf("Unexpected error %s", err.Error())
21 			}
22 		})
23 	}
24 }

Aquí el refactor. En este caso, gestiono el error provocando un panic, que no es la mejor manera de gestionar un error, pero que nos permite hacer que el test pueda fallar y obligarnos a implementar la solución.

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 	"strconv"
 7 )
 8 
 9 type Nif string
10 
11 func NewNif(candidate string) (Nif, error) {
12 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
13 
14 	if !valid.MatchString(candidate) {
15 		return "", errors.New("bad format")
16 	}
17 
18 	if candidate[8] == shouldHaveControl(candidate) {
19 		return Nif(candidate), nil
20 	}
21 
22 	return "", errors.New("bad control letter")
23 }
24 
25 func shouldHaveControl(candidate string) uint8 {
26 	const controlMap = "TRWAGMYFPDXBNJZSQVHLCKE"
27 
28 	numeric, err := strconv.Atoi(candidate[0:8])
29 
30 	if err != nil {
31 		panic("Numeric part contains letters")
32 	}
33 	
34 	modulus := numeric % 23
35 
36 	return controlMap[modulus]
37 }

Al ejecutar los tests, comprobamos que siguen en verde. Pero si reactivamos el último test vemos cómo falla:

 1 func TestShouldCreateNifTypeWithValidCandidate(t *testing.T) {
 2 	tests := []struct {
 3 		name string
 4 		example string
 5 	}{
 6 		{"should accept mod23 being 0", "00000000T"},
 7 		{"should accept mod23 being 0 letter T", "00000023T"},
 8 		{"should accept mod23 being 1 letter R", "00000024R"},
 9 		{"should accept NIE starting with X", "X0000023X"},
10 	}
11 	for _, test := range tests {
12 		t.Run(test.name, func(t *testing.T) {
13 			nif, err := NewNif(test.example)
14 
15 			if nif != Nif(test.example) {
16 				t.Errorf("Expected Nif(%s), got %s", test.example, nif)
17 			}
18 
19 			if err != nil {
20 				t.Errorf("Unexpected error %s", err.Error())
21 			}
22 		})
23 	}
24 }

Y esto ya nos obliga a introducir un tratamiento para estos casos. Básicamente, es reemplazar X por 0:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 	"strconv"
 7 	"strings"
 8 )
 9 
10 type Nif string
11 
12 func NewNif(candidate string) (Nif, error) {
13 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
14 
15 	if !valid.MatchString(candidate) {
16 		return "", errors.New("bad format")
17 	}
18 
19 	if candidate[8] == shouldHaveControl(candidate) {
20 		return Nif(candidate), nil
21 	}
22 
23 	return "", errors.New("bad control letter")
24 }
25 
26 func shouldHaveControl(candidate string) uint8 {
27 	const controlMap = "TRWAGMYFPDXBNJZSQVHLCKE"
28 
29 	var numPart = candidate[0:8]
30 
31 	if string(candidate[0]) == "X" {
32 		numPart = strings.Replace(numPart, "X", "0", 1)
33 	}
34 
35 	numeric, err := strconv.Atoi(numPart)
36 
37 	if err != nil {
38 		panic("Numeric part contains letters")
39 	}
40 
41 	modulus := numeric % 23
42 
43 	return controlMap[modulus]
44 }

Se puede refactorizar usando un Replacer:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 	"strconv"
 7 	"strings"
 8 )
 9 
10 type Nif string
11 
12 func NewNif(candidate string) (Nif, error) {
13 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
14 
15 	if !valid.MatchString(candidate) {
16 		return "", errors.New("bad format")
17 	}
18 
19 	if candidate[8] == shouldHaveControl(candidate) {
20 		return Nif(candidate), nil
21 	}
22 
23 	return "", errors.New("bad control letter")
24 }
25 
26 func shouldHaveControl(candidate string) uint8 {
27 	const controlMap = "TRWAGMYFPDXBNJZSQVHLCKE"
28 
29 	var numPart = candidate[0:8]
30 
31 	re := strings.NewReplacer("X", "0")
32 	
33 	numeric, err := strconv.Atoi(re.Replace(numPart))
34 
35 	if err != nil {
36 		panic("Numeric part contains letters")
37 	}
38 
39 	modulus := numeric % 23
40 
41 	return controlMap[modulus]
42 }

En este punto podríamos hacer un test para forzarnos a introducir el resto de reemplazos. Es barato, aunque en el fondo no es muy necesario por lo que comentamos antes: podemos interpretar esta parte del algoritmo como “reemplazar las letras iniciales X, Y y Z por los números 0, 1 y 2, respectivamente”:

 1 package nif
 2 
 3 import "testing"
 4 
 5 func TestShouldFailIfCandidateIsInvalid(t *testing.T) {
 6 	tests := []struct {
 7 		name string
 8 		example string
 9 		expected string
10 	}{
11 		{
12 			"should fail if too long",
13 			"01234567891011",
14 			"bad format",
15 		},
16 		{
17 			"should fail if too short",
18 			"01234",
19 			"bad format",
20 		},
21 		{
22 			"should fail if starts with a letter other than X, Y, Z",
23 			"A12345678",
24 			"bad format",
25 		},
26 		{
27 			"should fail if doesn't have 7 digit in the middle",
28 			"0123X567R",
29 			"bad format",
30 		},
31 		{
32 			"should fail if doesn't end with a valid control letter",
33 			"01234567U",
34 			"invalid end format",
35 		},
36 		{
37 			"should fail if doesn't end with the right control letter",
38 			"00000000S",
39 			"bad control letter",
40 		},
41 
42 	}
43 	for _, test := range tests {
44 		t.Run(test.name, func(t *testing.T) {
45 			_, err := NewNif(test.example)
46 
47 			if err.Error() != test.expected  && err.Error() != "bad format" {
48 				t.Errorf("Expected %s, got %s", test.expected, err.Error())
49 			}
50 		})
51 	}
52 }
53 
54 func TestShouldCreateNifTypeWithValidCandidate(t *testing.T) {
55 	tests := []struct {
56 		name string
57 		example string
58 	}{
59 		{"should accept mod23 being 0", "00000000T"},
60 		{"should accept mod23 being 0 letter T", "00000023T"},
61 		{"should accept mod23 being 1 letter R", "00000024R"},
62 		{"should accept NIE starting with X", "X0000000T"},
63 		{"should accept NIE starting with Y", "Y0000000Z"},
64 	}
65 	for _, test := range tests {
66 		t.Run(test.name, func(t *testing.T) {
67 			nif, err := NewNif(test.example)
68 
69 			if nif != Nif(test.example) {
70 				t.Errorf("Expected Nif(%s), got %s", test.example, nif)
71 			}
72 
73 			if err != nil {
74 				t.Errorf("Unexpected error %s", err.Error())
75 			}
76 		})
77 	}
78 }

Solo es necesario añadir los pares correspondientes:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 	"strconv"
 7 	"strings"
 8 )
 9 
10 type Nif string
11 
12 func NewNif(candidate string) (Nif, error) {
13 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
14 
15 	if !valid.MatchString(candidate) {
16 		return "", errors.New("bad format")
17 	}
18 
19 	if candidate[8] == shouldHaveControl(candidate) {
20 		return Nif(candidate), nil
21 	}
22 
23 	return "", errors.New("bad control letter")
24 }
25 
26 func shouldHaveControl(candidate string) uint8 {
27 	const controlMap = "TRWAGMYFPDXBNJZSQVHLCKE"
28 
29 	var numPart = candidate[0:8]
30 
31 	re := strings.NewReplacer("X", "0", 
32 	                          "Y", "1", 
33 	                          "Z", "2")
34 
35 	numeric, err := strconv.Atoi(re.Replace(numPart))
36 
37 	if err != nil {
38 		panic("Numeric part contains letters")
39 	}
40 
41 	modulus := numeric % 23
42 
43 	return controlMap[modulus]
44 }

Después de un rato de refactor, esta sería una posible solución:

 1 package nif
 2 
 3 import (
 4 	"errors"
 5 	"regexp"
 6 	"strconv"
 7 	"strings"
 8 )
 9 
10 type Nif string
11 
12 func NewNif(candidate string) (Nif, error) {
13 	valid := regexp.MustCompile(`(?i)^[0-9XYZ]\d{7}[^UIOÑ0-9]$`)
14 
15 	if !valid.MatchString(candidate) {
16 		return "", errors.New("bad format")
17 	}
18 
19 	if candidate[8] != controlLetterFor(candidate) {
20 		return "", errors.New("bad control letter")
21 	}
22 
23 	return Nif(candidate), nil
24 }
25 
26 func controlLetterFor(candidate string) uint8 {
27 	const controlMap = "TRWAGMYFPDXBNJZSQVHLCKE"
28 
29 	position, err := controlPosition(candidate[0:8])
30 
31 	if err != nil {
32 		panic("Numeric part contains letters")
33 	}
34 
35 	return controlMap[position]
36 }
37 
38 func controlPosition(numPart string) (int, error) {
39 	re := strings.NewReplacer("X", "0", "Y", "1", "Z", "2")
40 
41 	numeric, err := strconv.Atoi(re.Replace(numPart))
42 
43 	return numeric % 23, err
44 }

Qué hemos aprendido con esta kata

  • Utilizar sad paths para mover el desarrollo
  • Utilizar table tests en Go reduciendo el coste de añadir nuevos tests
  • Una técnica para cambiar los errores devueltos por otro más general sin romper los tests
  • Una técnica para cambiar la interfaz pública del código de producción sin romper los tests

Referencias

  • Is Go object-oriented?1