Prueba de solicitud HTTP: una herramienta de supervivencia para desarrolladores

Publicado: 2022-03-11

Qué hacer cuando una suite de pruebas no es factible

Hay momentos en que nosotros, los programadores y/o nuestros clientes, tenemos recursos limitados para escribir tanto el entregable esperado como las pruebas automatizadas para ese entregable. Cuando la aplicación es lo suficientemente pequeña, puede tomar atajos y omitir pruebas porque recuerda (principalmente) lo que sucede en otras partes del código cuando agrega una función, corrige un error o refactoriza. Dicho esto, no siempre trabajaremos con aplicaciones pequeñas, además, tienden a hacerse más grandes y complejas con el tiempo. Esto hace que las pruebas manuales sean difíciles y súper molestas.

Para mis últimos proyectos, me vi obligado a trabajar sin pruebas automatizadas y, sinceramente, fue vergonzoso que el cliente me enviara un correo electrónico después de un impulso de código para decirme que la aplicación se estaba rompiendo en lugares donde ni siquiera había tocado el código.

Entonces, en los casos en los que mi cliente no tenía presupuesto o no tenía la intención de agregar ningún marco de prueba automatizado, comencé a probar la funcionalidad básica de todo el sitio web enviando una solicitud HTTP a cada página individual, analizando los encabezados de respuesta y buscando el '200' respuesta. Suena simple y simple, pero hay muchas cosas que puede hacer para garantizar la fidelidad sin tener que escribir ninguna prueba, unidad, funcionalidad o integración.

Pruebas automatizadas

En el desarrollo web, las pruebas automatizadas comprenden tres tipos principales de pruebas: pruebas unitarias, pruebas funcionales y pruebas de integración. A menudo combinamos pruebas unitarias con pruebas funcionales y de integración para asegurarnos de que todo funcione sin problemas como una aplicación completa. Cuando estas pruebas se ejecutan al unísono o secuencialmente (preferiblemente con un solo comando o clic), comenzamos a llamarlas pruebas automatizadas, unitarias o no.

En gran medida, el propósito de estas pruebas (al menos en el desarrollo web) es asegurarse de que todas las páginas de la aplicación se representen sin problemas, sin errores o errores fatales (detención de la aplicación).

Examen de la unidad

La prueba unitaria es un proceso de desarrollo de software en el que las partes más pequeñas del código, las unidades, se prueban de forma independiente para verificar su correcto funcionamiento. Aquí hay un ejemplo en Ruby:

 test “should return active users” do active_user = create(:user, active: true) non_active_user = create(:user, active: false) result = User.active assert_equal result, [active_user] end

Pruebas funcionales

Las pruebas funcionales son una técnica utilizada para verificar las características y la funcionalidad del sistema o software, diseñadas para cubrir todos los escenarios de interacción del usuario, incluidas las rutas de falla y los casos límite.

Nota: todos nuestros ejemplos están en Ruby.

 test "should get index" do get :index assert_response :success assert_not_nil assigns(:object) end

Pruebas de integración

Una vez que los módulos son probados unitariamente, se integran uno a uno, secuencialmente, para verificar el comportamiento combinacional y validar que los requisitos se implementan correctamente.

 test "login and browse site" do # login via https https! get "/login" assert_response :success post_via_redirect "/login", username: users(:david).username, password: users(:david).password assert_equal '/welcome', path assert_equal 'Welcome david!', flash[:notice] https!(false) get "/articles/all" assert_response :success assert assigns(:articles) end

Pruebas en un mundo ideal

Las pruebas son ampliamente aceptadas en la industria y tienen sentido; las buenas pruebas te permiten:

  • Asegure la calidad de toda su aplicación con el mínimo esfuerzo humano
  • Identifique errores más fácilmente porque sabe exactamente dónde se está rompiendo su código debido a fallas en las pruebas
  • Cree documentación automática para su código
  • Evite el 'estreñimiento de codificación', que, según un tipo en Stack Overflow, es una forma humorística de decir, "cuando no sabe qué escribir a continuación, o tiene una tarea abrumadora frente a usted, comience escribiendo pequeño .”

Podría seguir y seguir sobre lo increíbles que son las pruebas y cómo cambiaron el mundo y bla bla bla, pero entiendes el punto. Conceptualmente, las pruebas son impresionantes.

Relacionado: Pruebas unitarias, Cómo escribir código comprobable y por qué es importante

Pruebas en el mundo real

Si bien los tres tipos de pruebas tienen ventajas, no se escriben en la mayoría de los proyectos. ¿Por qué? Bueno, déjame desglosarlo:

Tiempo/Plazos

Todo el mundo tiene fechas límite, y escribir pruebas nuevas puede ser un obstáculo para cumplir con una. Puede llevar tiempo y medio (o más) escribir una aplicación y sus respectivas pruebas. Ahora, algunos de ustedes no están de acuerdo con esto, citando el tiempo ahorrado en última instancia, pero no creo que este sea el caso y explicaré por qué en 'Diferencia de opinión'.

Problemas del cliente

A menudo, el cliente no entiende realmente qué es la prueba o por qué tiene valor para la aplicación. Los clientes tienden a estar más preocupados por la entrega rápida de productos y, por lo tanto, consideran que las pruebas programáticas son contraproducentes.

O puede ser tan simple como que el cliente no tenga el presupuesto para pagar el tiempo adicional necesario para implementar estas pruebas.

Falta de conocimiento

Hay una tribu considerable de desarrolladores en el mundo real que no sabe que existe la prueba. En cada conferencia, reunión, concierto (incluso en mis sueños), me encuentro con desarrolladores que no saben cómo escribir pruebas, no saben qué probar, no saben cómo configurar el marco para las pruebas, etc. en. Las pruebas no se enseñan exactamente en las escuelas, y puede ser una molestia configurar/aprender el marco para que se ejecuten. Así que sí, definitivamente hay una barrera de entrada.

'Es mucho trabajo'

Escribir pruebas puede ser abrumador tanto para los programadores nuevos como para los experimentados, incluso para aquellos tipos de genios que cambian el mundo, y para colmo, escribir pruebas no es emocionante. Uno puede pensar: "¿Por qué debería involucrarme en un trabajo aburrido y aburrido cuando podría estar implementando una función importante con resultados que impresionarán a mi cliente?" Es un argumento difícil.

Por último, pero no menos importante, es difícil escribir pruebas y los estudiantes de informática no están capacitados para ello.

Ah, y refactorizar con pruebas unitarias no es divertido.

Diferencia de opinión

En mi opinión, las pruebas unitarias tienen sentido para la lógica algorítmica, pero no tanto para coordinar el código vivo.

La gente afirma que, aunque esté invirtiendo tiempo extra por adelantado en escribir pruebas, le ahorra horas más tarde al depurar o cambiar el código. Discrepo y ofrezco una pregunta: ¿su código es estático o cambia constantemente?

Para la mayoría de nosotros, siempre está cambiando. Si está escribiendo un software exitoso, siempre está agregando funciones, cambiando las existentes, eliminándolas, comiéndolas, lo que sea, y para adaptarse a estos cambios, debe seguir cambiando sus pruebas, y cambiar sus pruebas lleva tiempo.

Pero, necesita algún tipo de prueba

Nadie argumentará que la falta de cualquier tipo de prueba es el peor de los casos posibles. Después de realizar cambios en su código, debe confirmar que realmente funciona. Muchos programadores intentan probar manualmente los conceptos básicos: ¿La página se muestra en el navegador? ¿Se está enviando el formulario? ¿Se muestra el contenido correcto? Y así sucesivamente, pero en mi opinión, esto es bárbaro, ineficiente y laborioso.

Lo que uso en su lugar

El propósito de probar una aplicación web, ya sea de forma manual o automatizada, es confirmar que cualquier página determinada se presenta en el navegador del usuario sin errores fatales y que muestra su contenido correctamente. Una forma (y en la mayoría de los casos, una forma más fácil) de lograr esto es enviar solicitudes HTTP a los puntos finales de la aplicación y analizar la respuesta. El código de respuesta le indica si la página se entregó correctamente. Es fácil probar el contenido analizando el cuerpo de respuesta de la solicitud HTTP y buscando coincidencias de cadenas de texto específicas, o puede ser un paso más elegante y usar bibliotecas de web scraping como nokogiri.

Si algunos puntos finales requieren un inicio de sesión de usuario, puede usar bibliotecas diseñadas para automatizar interacciones (ideal cuando se realizan pruebas de integración) como mecanizar para iniciar sesión o hacer clic en ciertos enlaces. Realmente, en el panorama general de las pruebas automatizadas, esto se parece mucho a las pruebas de integración o funcionales (dependiendo de cómo las use), pero es mucho más rápido de escribir y puede incluirse en un proyecto existente o agregarse a uno nuevo. , con menos esfuerzo que configurar todo el marco de prueba. ¡Correcto!

Relacionado: Contrate al 3% superior de los ingenieros de control de calidad independientes.

Los casos extremos presentan otro problema cuando se trata de grandes bases de datos con una amplia gama de valores; probar si nuestra aplicación funciona sin problemas en todos los conjuntos de datos anticipados puede ser desalentador.

Una forma de hacerlo es anticipar todos los casos extremos (que no solo es difícil, a menudo es imposible) y escribir una prueba para cada uno. Esto podría convertirse fácilmente en cientos de líneas de código (imagínense el horror) y engorroso de mantener. Sin embargo, con solicitudes HTTP y solo una línea de código, puede probar estos casos extremos directamente en los datos de producción, descargados localmente en su máquina de desarrollo o en un servidor de prueba.

Ahora, por supuesto, esta técnica de prueba no es una panacea y tiene muchas deficiencias, al igual que cualquier otro método, pero encuentro que este tipo de pruebas son más rápidas y fáciles de escribir y modificar.

En la práctica: Pruebas con solicitudes HTTP

Dado que ya establecimos que escribir código sin ningún tipo de pruebas que lo acompañen no es una buena idea, mi prueba básica para una aplicación completa es enviar solicitudes HTTP a todas sus páginas localmente y analizar los encabezados de respuesta para un 200 (o deseado) código.

Por ejemplo, si tuviéramos que escribir las pruebas anteriores (las que buscan contenido específico y un error fatal) con una solicitud HTTP en su lugar (en Ruby), sería algo como esto:

 # testing for fatal error http_code = `curl -X #{route[:method]} -s -o /dev/null -w "%{http_code}" #{Rails.application.routes.url_helpers.articles_url(host: 'localhost', port: 3000) }` if http_code !~ /200/ return “articles_url returned with #{http_code} http code.” end # testing for content active_user = create(:user, name: “user1”, active: true) non_active_user = create(:user, name: “user2”, active: false) content = `curl #{Rails.application.routes.url_helpers.active_user_url(host: 'localhost', port: 3000) }` if content !~ /#{active_user.name}/ return “Content mismatch active user #{active_user.name} not found in text body” #You can customise message to your liking end if content =~ /#{non_active_user.name}/ return “Content mismatch non active user #{active_user.name} found in text body” #You can customise message to your liking end

La línea curl -X #{route[:method]} -s -o /dev/null -w "%{http_code}" #{Rails.application.routes.url_helpers.articles_url(host: 'localhost', port: 3000) } cubre muchos casos de prueba; cualquier método que genere un error en la página del artículo se detectará aquí, por lo que efectivamente cubre cientos de líneas de código en una sola prueba.

La segunda parte, que detecta específicamente el error de contenido, se puede usar varias veces para verificar el contenido de una página. (Se pueden manejar solicitudes más complejas usando mechanize , pero eso está más allá del alcance de este blog).

Ahora, en los casos en los que desee probar si una página específica funciona en un conjunto grande y variado de valores de base de datos (por ejemplo, la plantilla de la página de su artículo funciona para todos los artículos en la base de datos de producción), puede hacer lo siguiente:

 ids = Article.all.select { |post| `curl -s -o /dev/null -w “%{http_code}” #{Rails.application.routes.url_helpers.article_url(post, host: 'localhost', port: 3000) }`.to_i != 200).map(&:id) return ids

Esto devolverá una matriz de ID de todos los artículos en la base de datos que no se procesaron, por lo que ahora puede ir manualmente a la página del artículo específico y verificar el problema.

Ahora, entiendo que esta forma de prueba puede no funcionar en ciertos casos, como probar un script independiente o enviar un correo electrónico, y es innegablemente más lento que las pruebas unitarias porque estamos haciendo llamadas directas a un punto final para cada prueba, pero cuando no puede tener pruebas unitarias, o pruebas funcionales, o ambas, esto es mejor que nada.

¿Cómo haría para estructurar estas pruebas? Con proyectos pequeños y no complejos, puede escribir todas sus pruebas en un archivo y ejecutar ese archivo cada vez antes de confirmar sus cambios, pero la mayoría de los proyectos requerirán un conjunto de pruebas.

Por lo general, escribo de dos a tres pruebas por punto final, según lo que esté probando. También puede intentar probar contenido individual (similar a la prueba unitaria), pero creo que sería redundante y lento, ya que realizará una llamada HTTP para cada unidad. Pero, por otro lado, serán más limpios y fáciles de entender.

Recomiendo poner sus pruebas en su carpeta de prueba normal con cada punto final principal que tenga su propio archivo (en Rails, por ejemplo, cada modelo/controlador tendría un archivo cada uno), y este archivo se puede dividir en tres partes según lo que deseemos. están probando A menudo tengo al menos tres pruebas:

prueba uno

Verifique que la página regrese sin errores fatales.

La prueba uno verifica que la página regrese sin errores fatales.

Tenga en cuenta cómo hice una lista de todos los puntos finales para Post y la iteré para verificar que cada página se represente sin ningún error. Suponiendo que todo salió bien y que todas las páginas se renderizaron, verá algo como esto en la terminal: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of failed url(s) -- []

Si alguna página no se procesa, verá algo como esto (en este ejemplo, la posts/index page tiene un error y, por lo tanto, no se procesa): ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of failed url(s) -- [{:url=>”posts_url”, :params=>[], :method=>”GET”, :http_code=>”500”}]

prueba dos

Confirme que todo el contenido esperado está allí:

La prueba dos confirma que todo el contenido esperado está ahí.

Si todo el contenido que esperamos se encuentra en la página, el resultado se ve así (en este ejemplo, nos aseguramos de que posts/:id tenga un título de publicación, una descripción y un estado): ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of content(s) not found on Post#show page with post id: 1 -- []

Si no se encuentra el contenido esperado en la página (aquí esperamos que la página muestre el estado de la publicación: 'Activo' si la publicación está activa, 'Deshabilitado' si la publicación está deshabilitada), el resultado se verá así: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of content(s) not found on Post#show page with post id: 1 -- [“Active”]

prueba tres

Verifique que la página se muestre en todos los conjuntos de datos (si corresponde):

La prueba 3 verifica que la página se represente en todos los conjuntos de datos.

Si todas las páginas se procesan sin ningún error, obtendremos una lista vacía: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of post(s) with error in rendering -- []

Si el contenido de algunos de los registros tiene un problema de representación (en este ejemplo, las páginas con ID 2 y 5 dan un error), el resultado se ve así: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of post(s) with error on rendering -- [2,5]

Si desea jugar con el código de demostración anterior, aquí está mi proyecto github.

Entonces, ¿cuál es mejor? Depende…

La prueba de solicitud HTTP podría ser su mejor apuesta si:

  • Estás trabajando con una aplicación web
  • Estás en una crisis de tiempo y quieres escribir algo rápido.
  • Está trabajando con un proyecto grande, un proyecto preexistente donde no se escribieron las pruebas, pero aún desea alguna forma de verificar el código
  • Su código implica una simple solicitud y respuesta.
  • No desea pasar una gran parte de su tiempo manteniendo las pruebas (leí en alguna parte prueba unitaria = infierno de mantenimiento, y estoy parcialmente de acuerdo con él / ella)
  • Desea probar si una aplicación funciona en todos los valores de una base de datos existente

Las pruebas tradicionales son ideales cuando:

  • Está tratando con algo que no es una aplicación web, como scripts
  • Estás escribiendo código algorítmico complejo
  • Tienes tiempo y presupuesto para dedicarte a escribir exámenes
  • El negocio requiere una tasa de error baja o libre de errores (finanzas, gran base de usuarios)

Gracias por leer el articulo; ahora debe tener un método de prueba que puede usar de manera predeterminada, uno con el que puede contar cuando tiene poco tiempo.

Relacionados:
  • Rendimiento y eficiencia: trabajar con HTTP/3
  • Manténgalo cifrado, manténgalo seguro: trabajar con ESNI, DoH y DoT