Estamos escribiendo una aplicación de aprendizaje en Go y Javascript para evaluar el rendimiento real de las acciones. Parte 2 - Prueba del backend

En la primera parte del artículo, escribimos un pequeño servidor web, que es el back-end de nuestro sistema de información. Esa parte no fue particularmente interesante, aunque demostró el uso de la interfaz y uno de los métodos para trabajar con gorutinas. Tanto eso como otro pueden ser interesantes para los desarrolladores principiantes.

La segunda parte es mucho más interesante y útil, porque en ella escribiremos pruebas unitarias tanto para el servidor como para el paquete de la biblioteca que implementa el almacén de datos. Empecemos


foto de aqui

Entonces, permítame recordarle que nuestra aplicación consta de un módulo ejecutable (servidor web, API), módulo de almacenamiento (estructuras de datos de entidad, interfaz de contrato para proveedores de almacenamiento) y módulos de proveedor de almacenamiento (en nuestro ejemplo, solo hay un módulo que ejecuta la interfaz de almacenamiento datos en memoria).

Probaremos el módulo ejecutable y la implementación de almacenamiento. El módulo de contrato no contiene código que pueda ser probado. Solo hay declaraciones de tipo.
Para las pruebas, utilizaremos solo las capacidades de la biblioteca estándar: paquetes de prueba y httptest. En mi opinión, son suficientes, aunque hay muchos marcos de prueba diferentes. Míralos, tal vez te gusten. Desde mi punto de vista, los programas en Go realmente no necesitan esos marcos (de varios tipos) que existen actualmente. Este no es Javascript para usted, que se discutirá en la tercera parte del artículo.

Primero, algunas palabras sobre la metodología de prueba que uso para los programas Go.

En primer lugar , debo decir que realmente me gusta Go solo porque no lleva al programador a un marco rígido. Aunque a algunos desarrolladores, para ser justos, les encanta conducirse al marco traído del PL anterior. Digamos, el mismo Rob Pike, dijo que no veía ningún problema en copiar el código, si eso era más fácil. Tal copiar y pegar está incluso en la biblioteca estándar. En lugar de importar el paquete, uno de los autores del lenguaje simplemente copió el texto de una función (verificación Unicode). En esta prueba, el paquete Unicode se importa, por lo que todo está bien.

Por cierto, en este sentido (en el sentido de la flexibilidad del lenguaje), se puede utilizar una técnica interesante al escribir pruebas. La conclusión es esta: sabemos que los contratos de interfaz en Go se ejecutan implícitamente. Es decir, podemos declarar un tipo, escribir métodos para él y ejecutar algún tipo de contrato. Quizás incluso sin saberlo. Esto es conocido y entendido. Sin embargo, esto también funciona en la dirección opuesta. Si el autor de algún módulo no escribió una interfaz que nos ayudaría a crear un código auxiliar para probar nuestro paquete, entonces podemos declarar la interfaz en nuestra prueba, que se ejecutará en un paquete de terceros. Una idea fructífera, aunque no es útil en nuestra aplicación de capacitación.

En segundo lugar , algunas palabras sobre el momento de escribir las pruebas. Como todos saben, hay diferentes opiniones sobre cuándo escribir pruebas unitarias. Las ideas principales son las siguientes:

  • Escribimos pruebas antes de escribir el código (TDD). Por lo tanto, entendemos mejor la tarea y establecemos criterios de calidad.
  • Escribimos pruebas mientras escribimos código, o incluso un poco más tarde (consideraremos este prototipo incremental).
  • Escribiremos pruebas en algún momento posterior, si hay tiempo. Y esto no es una broma. A veces las condiciones son tales que físicamente no hay tiempo.

No creo que haya la única opinión correcta sobre este tema. Compartiré el mío y pediré a los lectores que comenten en los comentarios. Mi opinión es esta:

  • Desarrollar paquetes independientes en TDD, realmente simplifica el asunto, especialmente cuando el lanzamiento de la aplicación para verificación es un proceso que requiere muchos recursos. Por ejemplo, recientemente desarrollé un sistema de monitoreo de vehículos GPS / GLONASS. Los paquetes de controladores para protocolos solo se pueden desarrollar a través de pruebas, ya que el lanzamiento y la verificación manual de una aplicación requieren la espera de datos de los rastreadores, lo cual es extremadamente inconveniente. Para las pruebas, tomé muestras de paquetes de datos, las grabé en pruebas de tabla y no inicié el servidor hasta que los controladores estuvieron listos.
  • Si la estructura del código no está clara, entonces primero intento hacer un prototipo funcional mínimo. Luego escribo pruebas, o incluso primero pulido el código un poco y luego solo las pruebas.
  • Para los módulos ejecutables, primero escribo un prototipo. Pruebas posteriores. No pruebo el código ejecutable obvio en absoluto (puede escribir el inicio del servidor http desde main en una función separada y llamarlo en la prueba, pero ¿por qué probar la biblioteca estándar?)

En base a esto, en nuestra aplicación de capacitación, el proveedor de almacenamiento RAM fue escrito a través de pruebas. El ejecutable del servidor se creó a través de un prototipo.

Comencemos con las pruebas para implementar el repositorio.

En el repositorio, tenemos el método de fábrica New (), que devuelve un puntero a una instancia del tipo de almacenamiento. También hay métodos para obtener cotizaciones de Valores (), agregar papel a la lista Agregar () e inicializar el almacenamiento con datos del servidor Mosbirzh InitData ().

Prueba del constructor (los términos OOP se usan libremente, de manera informal. De acuerdo con la posición de OOP en Go).

//    func TestNew(t *testing.T) { //   - memoryStorage := New() //     var s *Storage //         .   if reflect.TypeOf(memoryStorage) != reflect.TypeOf(s) { t.Errorf(" :  %v,   %v", reflect.TypeOf(memoryStorage), reflect.TypeOf(s)) } //     t.Logf("\n%+v\n\n", memoryStorage) } 

En esta prueba, sin una necesidad especial, se demostró que la única forma en Go de verificar el tipo de una variable es la reflexión (reflect.TypeOf (memoryStorage)). No se recomienda el uso excesivo de este módulo. Los desafíos son pesados ​​y no valen la pena. Por otro lado, ¿qué más verificar en esta prueba además de la ausencia de un error?

A continuación, probamos el recibo de cotizaciones y la adición de papel. Estas pruebas se duplican parcialmente entre sí, pero esto no es crítico (en la prueba para agregar papel, se llama al método para obtener cotizaciones para la verificación). En general, a veces escribo una prueba para todas las operaciones CRUD para una entidad en particular. Es decir, en la prueba creo una entidad, la leo, la cambio, la leo de nuevo, la borro, la leo de nuevo. No es muy elegante, pero los defectos obvios no son visibles.

Prueba de cotización.

 //    func TestSecurities(t *testing.T) { //     var s *Storage //    ss, err := s.Securities() if err != nil { t.Error(err) } //     t.Logf("\n%+v\n\n", ss) } } 

Todo es bastante obvio aquí.

Ahora prueba para agregar papel. En esta prueba, con fines educativos (sin necesidad real), utilizaremos una técnica de prueba de mesa muy conveniente. Su esencia es la siguiente: creamos una matriz de estructuras sin nombre, cada una de las cuales contiene los datos de entrada para la prueba y el resultado esperado. En nuestro caso, presentamos una garantía para agregar, el resultado es el número de valores en la bóveda (longitud de la matriz). A continuación, realizamos una prueba para cada elemento de la matriz de estructuras (llame al método de prueba con los datos de entrada del elemento) y comparemos el resultado con el campo de resultados del elemento actual. Resulta algo como esto.

 //    func TestAdd(t *testing.T) { //     var s *Storage var security = storage.Security{ ID: "MSFT", } //   var tt = []struct { s storage.Security //   length int //   () }{ { s: security, length: 1, }, { s: security, length: 2, }, } var ss []storage.Security // tc - test case, tt - table tests for _, tc := range tt { //    err := s.Add(security) if err != nil { t.Error(err) } ss, err = s.Securities() if err != nil { t.Error(err) } if len(ss) != tc.length { t.Errorf("  :  %d,   %d", len(ss), tc.length) } } //     t.Logf("\n%+v\n\n", ss) } 

Bueno, una prueba para la función de inicialización de datos.

 //    func TestInitData(t *testing.T) { //     var s *Storage //    err := s.InitData() if err != nil { t.Error(err) } ss, err := s.Securities() if err != nil { t.Error(err) } if len(ss) < 1 { t.Errorf(" :  %d,   '> 1'", len(ss)) } //     t.Logf("\n%+v\n\n", ss[0]) } 

Como resultado de la ejecución exitosa de la prueba, obtenemos: 17.595s de cobertura: 86.0% de las declaraciones.

Puede decir que sería bueno que una biblioteca separada obtuviera una cobertura del 100%, pero específicamente aquí las rutas de ejecución fallidas (errores en las funciones) son imposibles en absoluto, debido a las características de implementación: todo está en la memoria, no estamos conectados en ningún lugar, no dependemos de nada. Existe un manejo formal de errores, ya que un contrato de interfaz hace que se devuelva el error y la interfaz lo requiere.

Pasemos a probar el paquete ejecutable: el servidor web. Hay que decir que, dado que el servidor web es una construcción súper estándar en los programas Go, el paquete "net / http / httptest" se desarrolló especialmente para probar controladores de solicitudes http. Le permite simular un servidor web, ejecutar un controlador de solicitudes y registrar la respuesta del servidor web en una estructura especial. Lo usaremos, es muy simple, seguro que te gustará.

Al mismo tiempo, existe la opinión (y no solo la mía) de que dicha prueba puede no ser muy relevante para un sistema de trabajo real. En principio, puede iniciar un servidor real y llamar a controladores de conexión reales en las pruebas.

Es cierto que existe otra opinión (y no solo la mía) de que aislar la lógica empresarial de los sistemas para manipular datos reales es bueno.

En este sentido, podemos decir que estamos escribiendo pruebas unitarias, no pruebas de integración que involucran otros paquetes y servicios. Aunque aquí también soy de la opinión de que cierta flexibilidad de Go le permite no centrarse en los términos y escribir las pruebas que sean más adecuadas para sus tareas. Permítanme darles un ejemplo: para las pruebas de los manejadores de solicitudes API, hice una copia simplificada de la base de datos en un servidor real en la red, inicialicé con un pequeño conjunto de datos y realicé pruebas en datos reales. Pero este enfoque es muy situacional.

De vuelta a las pruebas de nuestro servidor web. Para poder escribir pruebas que sean independientes del almacenamiento real, necesitamos desarrollar un almacenamiento auxiliar. Esto no es difícil en absoluto, ya que trabajamos con el repositorio a través de la interfaz (consulte la primera parte). Todo lo que tenemos que hacer es declarar algunos tipos de datos propios e implementar los métodos del contrato de la interfaz de almacenamiento, incluso con datos vacíos. Algo como esto:

 //    -    type stub int //      var securities []storage.Security //    // ******************************* //     // InitData      func (s *stub) InitData() (err error) { //   -   var security = storage.Security{ ID: "MSFT", Name: "Microsoft", IssueDate: 1514764800, // 01/01/2018 } var quote = storage.Quote{ SecurityID: "MSFT", Num: 0, TimeStamp: 1514764800, Price: 100, } security.Quotes = append(security.Quotes, quote) securities = append(securities, security) return err } // Securities      func (s *stub) Securities() (data []storage.Security, err error) { return securities, err } //   // ***************** 

Ahora podemos inicializar nuestro almacenamiento con un trozo. Como hacerlo Con el fin de inicializar el entorno de prueba en Go de alguna versión no muy antigua, se agregó una función:

 func TestMain(m *testing.M) 

Esta función le permite inicializar y ejecutar todas las pruebas. Se parece a esto:

 //    -   func TestMain(m *testing.M) { //     -    db = new(stub) //   () db.InitData() //     os.Exit(m.Run()) } 

Ahora podemos escribir pruebas para manejadores de solicitudes API. Tenemos dos puntos finales de API, dos controladores y, por lo tanto, dos pruebas. Son muy similares, así que aquí está el primero de ellos.

 //    func TestSecuritiesHandler(t *testing.T) { //     req, err := http.NewRequest(http.MethodGet, "/api/v1/securities", nil) if err != nil { t.Fatal(err) } // ResponseRecorder    rr := httptest.NewRecorder() handler := http.HandlerFunc(securitiesHandler) //       handler.ServeHTTP(rr, req) //  HTTP-  if rr.Code != http.StatusOK { t.Errorf(" :  %v,   %v", rr.Code, http.StatusOK) } //  ()     json    var ss []storage.Security err = json.NewDecoder(rr.Body).Decode(&ss) if err != nil { t.Fatal(err) } //       t.Logf("\n%+v\n\n", ss) } 

La esencia de la prueba es la siguiente: cree una solicitud http, defina una estructura para registrar la respuesta del servidor, inicie el controlador de solicitudes, decodifique el cuerpo de la respuesta (json en la estructura). Bueno, para mayor claridad, imprimimos la respuesta.

Resulta algo así como:
=== EJECUTAR TestSecuritiesHandler
0xc00005e3e0
- PASS: TestSecuritiesHandler (0.00s)
c: \ Users \ dtsp \ YandexDisk \ go \ src \ moex_etf \ server \ server_test.go: 96:
[{ID: Nombre de MSFT: Fecha de emisión de Microsoft: 1514764800 Cotizaciones: [{SecurityID: MSFT Num: 0 TimeStamp: 1514764800 Price: 100}]}]

Pase
ok moex_etf / server 0.307s
Éxito: se aprobaron las pruebas.
Código Github

En la siguiente parte final del artículo, desarrollaremos una aplicación web para mostrar gráficos de rendimientos de acciones reales en ETF de Moscow Exchange.

Source: https://habr.com/ru/post/440568/


All Articles