En lenguajes din谩micos, como python y javascript, es posible reemplazar m茅todos y clases en m贸dulos directamente durante la operaci贸n. Esto es muy conveniente para las pruebas: simplemente puede poner "parches" que excluir谩n la l贸gica pesada o innecesaria en el contexto de esta prueba.
驴Pero qu茅 hacer en C ++? Ir? Java? En estos idiomas, el c贸digo no puede modificarse para las pruebas sobre la marcha, y la creaci贸n de parches requiere herramientas separadas.
En tales casos, debe escribir espec铆ficamente el c贸digo para que se pruebe. Esto no es solo un deseo man铆aco de ver una cobertura del 100% en su proyecto. Este es un paso hacia la escritura de c贸digo compatible y de calidad.
En este art铆culo, tratar茅 de hablar sobre las ideas principales detr谩s de la escritura de c贸digo comprobable y mostrar c贸mo se pueden usar con un ejemplo de un programa simple.
Programa sin complicaciones
Escribiremos un programa simple para realizar una solicitud a la API de VK. Este es un programa bastante simple que genera una solicitud, la hace, lee la respuesta, decodifica la respuesta de JSON en una estructura y muestra el resultado al usuario.
package main import ( "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" ) const token = "token here" func main() {
Como profesionales en nuestro campo, decidimos que era necesario escribir pruebas para nuestra aplicaci贸n. Crea un archivo de prueba ...
package main import ( "testing" ) func Test_Main(t *testing.T) { main() }
No se ve muy atractivo. Esta comprobaci贸n es un simple lanzamiento de una aplicaci贸n en la que no podemos influir. No podemos excluir el trabajo con la red, verificar la operabilidad de varios errores e incluso reemplazar el token para la verificaci贸n fallar谩. Intentemos descubrir c贸mo mejorar este programa.
Patr贸n de inyecci贸n de dependencia
Primero debe implementar el patr贸n de "inyecci贸n de dependencia" .
type VKClient struct { Token string } func (client VKClient) ShowUserInfo() { var requestURL = fmt.Sprintf( "https://api.vk.com/method/%s?&access_token=%s&v=5.95", "users.get", client.Token, )
Al agregar una estructura, creamos una dependencia (clave de acceso) para la aplicaci贸n, que puede transferirse desde diferentes fuentes, lo que evita los valores "cableados" y simplifica las pruebas.
package example import ( "testing" ) const workingToken = "workingToken" func Test_ShowUserInfo_Successful(t *testing.T) { client := VKClient{workingToken} client.ShowUserInfo() } func Test_ShowUserInfo_EmptyToken(t *testing.T) { client := VKClient{""} client.ShowUserInfo() }
Ahora solo una persona puede cometer un error, y solo si sabe cu谩l deber铆a ser la conclusi贸n. Para resolver este problema, es necesario no enviar informaci贸n directamente a la secuencia de salida, sino agregar m茅todos separados para obtener informaci贸n y su salida. Estas dos partes independientes ser谩n m谩s f谩ciles de verificar y mantener.
GetUserInfo()
m茅todo GetUserInfo()
, que devolver谩 una estructura con informaci贸n del usuario y un error (si sucedi贸). Dado que este m茅todo no genera nada, los errores que ocurren se transmitir谩n a煤n m谩s sin salida, de modo que el c贸digo que necesita los datos resolver谩 la situaci贸n.
type UserInfo struct { ID int `json:"id"` FirstName string `json:"first_name"` LastName string `json:"last_name"` } func (client VKClient) GetUserInfo() (UserInfo, error) { var requestURL = fmt.Sprintf( "https://api.vk.com/method/%s?&access_token=%s&v=5.95", "users.get", client.Token, ) resp, err := http.PostForm(requestURL, nil) if err != nil { return UserInfo{}, err }
Cambie ShowUserInfo()
para que use GetUserInfo()
y maneje los errores.
func (client VKClient) ShowUserInfo() { userInfo, err := client.GetUserInfo() if err != nil { fmt.Println(err) return } fmt.Printf( "Your id: %d\nYour full name: %s %s\n", userInfo.ID, userInfo.FirstName, userInfo.LastName, ) }
Ahora en las pruebas puede verificar que se recibe la respuesta correcta del servidor, y si el token es incorrecto, se devuelve un error.
func Test_GetUserInfo_Successful(t *testing.T) { client := VKClient{workingToken} userInfo, err := client.GetUserInfo() if err != nil { t.Fatal(err) } if userInfo.ID == 0 { t.Fatal("ID is empty") } if userInfo.FirstName == "" { t.Fatal("FirstName is empty") } if userInfo.LastName == "" { t.Fatal("LastName is empty") } } func Test_ShowUserInfo_EmptyToken(t *testing.T) { client := VKClient{""} _, err := client.GetUserInfo() if err == nil { t.Fatal("Expected error but found <nil>") } if err.Error() != "No values in response array" { t.Fatalf(`Expected "No values in response array", but found "%s"`, err) } }
Adem谩s de actualizar las pruebas existentes, debe agregar nuevas pruebas para el m茅todo ShowUserInfo()
.
func Test_ShowUserInfo(t *testing.T) { client := VKClient{workingToken} client.ShowUserInfo() } func Test_ShowUserInfo_WithError(t *testing.T) { client := VKClient{""} client.ShowUserInfo() }
Alternativas personalizadas
Las pruebas para ShowUserInfo()
parecen a lo que intentamos escapar inicialmente. En este caso, el 煤nico punto del m茅todo es enviar informaci贸n a la secuencia de salida est谩ndar. Por un lado, puede intentar redefinir os.Stdout y verificar la salida, parece una soluci贸n demasiado redundante cuando puede actuar con m谩s elegancia.
En lugar de usar fmt.Printf
, puede usar fmt.Fprintf
, que le permite io.Writer
a cualquier io.Writer
. os.Stdout
implementa esta interfaz, que nos permite reemplazar fmt.Printf(text)
con fmt.Fprintf(os.Stdout, text)
. Despu茅s de eso, podemos colocar os.Stdout
en un campo separado, que se puede establecer en los valores deseados (para pruebas, una cadena, para el trabajo, un flujo de salida est谩ndar).
Dado que la capacidad de cambiar Writer para la salida rara vez se utilizar谩, principalmente para pruebas, tiene sentido establecer un valor predeterminado. A continuaci贸n, para esto haremos esto: hacer que el tipo VKClient
exportable y crear una funci贸n de constructor para 茅l.
type vkClient struct { Token string OutputWriter io.Writer } func CreateVKClient(token string) vkClient { return vkClient{ token, os.Stdout, } }
En la funci贸n ShowUserInfo()
, reemplazamos las llamadas Print
con Fprintf
.
func (client vkClient) ShowUserInfo() { userInfo, err := client.GetUserInfo() if err != nil { fmt.Fprintf(client.OutputWriter, err.Error()) return } fmt.Fprintf( client.OutputWriter, "Your id: %d\nYour full name: %s %s\n", userInfo.ID, userInfo.FirstName, userInfo.LastName, ) }
Ahora necesita actualizar las pruebas para que creen el cliente utilizando el constructor e instalen otro escritor cuando sea necesario.
func Test_ShowUserInfo(t *testing.T) { client := CreateVKClient(workingToken) buffer := bytes.NewBufferString("") client.OutputWriter = buffer client.ShowUserInfo() result, _ := ioutil.ReadAll(buffer) matched, err := regexp.Match( `Your id: \d+\nYour full name: [^\n]+\n`, result, ) if err != nil { t.Fatal(err) } if !matched { t.Fatalf(`Expected match but failed with "%s"`, result) } } func Test_ShowUserInfo_WithError(t *testing.T) { client := CreateVKClient("") buffer := bytes.NewBufferString("") client.OutputWriter = buffer client.ShowUserInfo() result, _ := ioutil.ReadAll(buffer) if string(result) != "No values in response array" { t.Fatal("Wrong error") } }
Para cada prueba en la que producimos algo, creamos un b煤fer que desempe帽ar谩 el papel de una secuencia de salida est谩ndar. Despu茅s de ejecutar la funci贸n, se verifica que los resultados se correspondan con nuestras expectativas, con la ayuda de expresiones regulares o una simple comparaci贸n.
驴Por qu茅 estoy usando expresiones regulares? Para que las pruebas funcionen con cualquier token v谩lido que proporcionar茅 al programa, independientemente del nombre de usuario y la ID de usuario.
Patr贸n de inyecci贸n de dependencia - 2
Por el momento, el programa tiene una cobertura del 86,4%. 驴Por qu茅 no 100%? No podemos provocar errores de http.PostForm()
, ioutil.ReadAll()
y json.Unmarshal()
, lo que significa que no podemos verificar cada " return UserInfo, err
".
Para tener a煤n m谩s control sobre la situaci贸n, debe crear una interfaz en la que se ajuste http.Client
, http.Client
implementaci贸n estar谩 en vkClient y se utilizar谩 para las operaciones de red. Para nosotros, en la interfaz, solo un m茅todo es PostForm
: PostForm
.
type Networker interface { PostForm(string, url.Values) (*http.Response, error) } type vkClient struct { Token string OutputWriter io.Writer Networker Networker } func CreateVKClient(token string) vkClient { return vkClient{ token, os.Stdout, &http.Client{}, } }
Tal movimiento elimina la necesidad de realizar operaciones de red en general. Ahora podemos simplemente devolver los datos esperados de VKontakte usando el falso Networker
. Por supuesto, no se deshaga de las pruebas que verificar谩n las solicitudes al servidor, pero no es necesario realizar solicitudes en cada prueba.
Crearemos implementaciones para el Networker
y Reader
falsos, de modo que podamos probar los errores en cada caso, a pedido, al leer el cuerpo y durante la deserializaci贸n. Si queremos un error al llamar a PostForm, simplemente lo devolvemos en este m茅todo. Si queremos un error
al leer el cuerpo de la respuesta, es necesario devolver un Reader
falso, que arrojar谩 un error. Y si necesitamos que el error se manifieste durante la deserializaci贸n, entonces devolvemos la respuesta con una cadena vac铆a en el cuerpo. Si no queremos ning煤n error, simplemente devolvemos el cuerpo con el contenido especificado.
type fakeReader struct{} func (fakeReader) Read(p []byte) (n int, err error) { return 0, errors.New("Error on read") } type fakeNetworker struct { ErrorOnPostForm bool ErrorOnBodyRead bool ErrorOnUnmarchal bool RawBody string } func (fn *fakeNetworker) PostForm(string, url.Values) (*http.Response, error) { if fn.ErrorOnPostForm { return nil, fmt.Errorf("Error on PostForm") } if fn.ErrorOnBodyRead { return &http.Response{Body: ioutil.NopCloser(fakeReader{})}, nil } if fn.ErrorOnUnmarchal { fakeBody := ioutil.NopCloser(bytes.NewBufferString("")) return &http.Response{Body: fakeBody}, nil } fakeBody := ioutil.NopCloser(bytes.NewBufferString(fn.RawBody)) return &http.Response{Body: fakeBody}, nil }
Para cada situaci贸n problem谩tica, agregamos una prueba. Crear谩n un Networker
falso con la configuraci贸n necesaria, seg煤n la cual arrojar谩 un error en un momento determinado. Despu茅s de eso, llamamos a la funci贸n para que se verifique y nos aseguramos de que ocurriera un error, y de que esper谩bamos este error.
func Test_GetUserInfo_ErrorOnPostForm(t *testing.T) { client := CreateVKClient(workingToken) client.Networker = &fakeNetworker{ErrorOnPostForm: true} _, err := client.GetUserInfo() if err == nil { t.Fatal("Expected error but none found") } if err.Error() != "Error on PostForm" { t.Fatalf(`Expected "Error on PostForm" but got "%s"`, err.Error()) } } func Test_GetUserInfo_ErrorOnBodyRead(t *testing.T) { client := CreateVKClient(workingToken) client.Networker = &fakeNetworker{ErrorOnBodyRead: true} _, err := client.GetUserInfo() if err == nil { t.Fatal("Expected error but none found") } if err.Error() != "Error on read" { t.Fatalf(`Expected "Error on read" but got "%s"`, err.Error()) } } func Test_GetUserInfo_ErrorOnUnmarchal(t *testing.T) { client := CreateVKClient(workingToken) client.Networker = &fakeNetworker{ErrorOnUnmarchal: true} _, err := client.GetUserInfo() if err == nil { t.Fatal("Expected error but none found") } const expectedError = "unexpected end of JSON input" if err.Error() != expectedError { t.Fatalf(`Expected "%s" but got "%s"`, expectedError, err.Error()) } }
Usando el campo RawBody
, puede deshacerse de las solicitudes de red (solo devuelva lo que esperamos recibir de VKontakte). Esto puede ser necesario para evitar exceder los l铆mites de consulta durante las pruebas o para acelerar las pruebas.
Resumen
Despu茅s de todas las operaciones en el proyecto, recibimos un paquete de 91 l铆neas de largo (+170 l铆neas de pruebas), que admite la salida a cualquier io.Writer
, le permite usar m茅todos alternativos de trabajo con la red (usando el adaptador a nuestra interfaz), en el que hay un m茅todo como para generar datos y obtenerlos. El proyecto tiene una cobertura del 100%. Las pruebas verifican completamente cada respuesta de l铆nea y aplicaci贸n a cada posible error.
Cada paso en el camino hacia una cobertura del 100% aument贸 la modularidad, el mantenimiento y la confiabilidad de la aplicaci贸n, por lo que no hay nada de malo en el hecho de que las pruebas dictaron la estructura del paquete.
La capacidad de prueba de cualquier c贸digo es una cualidad que no aparece desde el aire. La capacidad de prueba aparece cuando el desarrollador usa patrones adecuadamente en situaciones apropiadas y escribe c贸digo personalizado y modular. La tarea principal era mostrar el proceso de pensamiento al realizar programas de refactorizaci贸n. Un pensamiento similar puede extenderse a cualquier aplicaci贸n y biblioteca, as铆 como a otros idiomas.