Tarde o temprano, todos los desarrolladores deben interactuar con una API. La parte más difícil siempre está relacionada con la prueba confiable del código que escribimos y, como queremos asegurarnos de que todo funcione correctamente, ejecutamos continuamente el código que consulta la API en sí. Este proceso es lento e ineficiente, ya que podemos experimentar problemas de red e inconsistencias en los datos (los resultados de la API pueden cambiar). Repasemos cómo podemos evitar todo este esfuerzo con Ruby..
"El flujo es esencial: escriba las pruebas, ejecútelas y vea cómo fallan, luego escriba el código de implementación mínimo para hacerlas aprobar. Una vez que todas lo hagan, refactorice si es necesario".
Nuestro objetivo es simple: escribir un pequeño envoltorio alrededor de la API de Dribbble para recuperar información sobre un usuario (llamado "jugador" en el mundo de Dribbble).
Como usaremos Ruby, también seguiremos un enfoque TDD: si no está familiarizado con esta técnica, Nettuts + tiene una buena introducción a RSpec que puede leer. En pocas palabras, escribiremos pruebas antes de escribir nuestra implementación de código, para que sea más fácil detectar errores y lograr una alta calidad de código. El flujo es esencial: escriba las pruebas, ejecútelas y vea cómo fallan, luego escriba el código de implementación mínimo para hacerlas aprobar. Una vez que todos lo hacen, refactorizar si es necesario.
La API de Dribbble es bastante sencilla. En el momento de esto, solo admite solicitudes GET y no requiere autenticación: un candidato ideal para nuestro tutorial. Además, ofrece un límite de 60 llamadas por minuto, una restricción que muestra perfectamente por qué trabajar con API requiere un enfoque inteligente..
Este tutorial debe suponer que está familiarizado con los conceptos de prueba: accesorios, simulacros, expectativas. Las pruebas son un tema importante (especialmente en la comunidad Ruby) e incluso si no eres un Rubyist, te animo a profundizar en el tema y buscar herramientas equivalentes para tu idioma cotidiano. Si lo desea, lea "El libro RSpec" de David Chelimsky et al., Una excelente introducción a Behavior Driven Development..
Para resumir aquí, aquí hay tres conceptos clave que debe saber:
"Como práctica general, ejecute pruebas cada vez que las actualice".
WebMock es una biblioteca de simulacros de Ruby que se utiliza para simular (o código auxiliar) las solicitudes de http. En otras palabras, le permite simular cualquier solicitud HTTP sin hacer una. La principal ventaja de esto es poder desarrollar y probar contra cualquier servicio HTTP sin necesitar el servicio en sí mismo y sin incurrir en problemas relacionados (como límites de API, restricciones de IP y demás).
La videograbadora es una herramienta complementaria que registra cualquier solicitud http real y crea un dispositivo, un archivo que contiene todos los datos necesarios para replicar esa solicitud sin volver a realizarla. Lo configuraremos para usar WebMock para hacer eso. En otras palabras, nuestras pruebas interactuarán con la API real de Dribbble solo una vez: después de eso, WebMock eliminará todas las solicitudes gracias a los datos registrados por VCR. Tendremos una réplica perfecta de las respuestas de la API de Dribbble grabadas localmente. Además, WebMock nos permitirá probar casos perimetrales (como el tiempo de espera de la solicitud) de manera fácil y consistente. Una maravillosa consecuencia de nuestra configuración es que todo será extremadamente rápido..
En cuanto a las pruebas unitarias, utilizaremos Minitest. Es una biblioteca de pruebas unitarias rápida y simple que también respalda las expectativas en el modo RSpec. Ofrece un conjunto de características más pequeño, pero me parece que esto realmente lo alienta y lo empuja a separar su lógica en métodos pequeños y comprobables. Minitest es parte de Ruby 1.9, por lo que si lo está utilizando (espero que sí) no necesita instalarlo. En Ruby 1.8, es sólo una cuestión de gema instalar minitest
.
Usaré Ruby 1.9.3: si no lo hace, probablemente encontrará algunos problemas relacionados con require_relative
, pero he incluido el código de reserva en un comentario justo debajo de él. Como práctica general, debe ejecutar pruebas cada vez que las actualice, incluso si no menciono este paso explícitamente en el tutorial..
Utilizaremos lo convencional. / lib
y /especulación
Estructura de carpetas para organizar nuestro código. En cuanto al nombre de nuestra biblioteca, lo llamaremos. Plato, siguiendo la convención de Dribbble de usar términos relacionados con el baloncesto.
El Gemfile contendrá todas nuestras dependencias, aunque son bastante pequeñas.
fuente: rubygems gema grupo 'httparty': prueba hacer gema 'webmock' gema 'vcr' gema 'girar' gema 'rastrillo' final
Httparty es una gema fácil de usar para manejar solicitudes HTTP; Será el núcleo de nuestra biblioteca. En el grupo de prueba, también agregaremos Turn para cambiar la salida de nuestras pruebas para que sean más descriptivas y admitan el color..
los / lib
y /especulación
Las carpetas tienen una estructura simétrica: para cada archivo contenido en el / lib / plato
carpeta, debe haber un archivo dentro / spec / plato
con el mismo nombre y el sufijo '_spec'.
Empecemos por crear una /lib/dish.rb
Archivo y añadir el siguiente código:
requiere "httparty" Dir [File.dirname (__ FILE__) + '/dish/*.rb'◆.each do | file | requiere el final del archivo
No hace mucho: requiere 'httparty' y luego itera sobre cada .rb
archivo dentro / lib / plato
para exigirlo. Con este archivo en su lugar, podremos agregar cualquier funcionalidad dentro de archivos separados en / lib / plato
y que se cargue automáticamente solo por requerir este único archivo.
Vamos a la /especulación
carpeta. Aquí está el contenido de la spec_helper.rb
expediente.
# necesitamos el archivo de biblioteca real require_relative '… / lib / dish' # para Ruby < 1.9.3, use this instead of require_relative # require(File.expand_path('… /… /lib/dish', __FILE__)) #dependencies require 'minitest/autorun' require 'webmock/minitest' require 'vcr' require 'turn' Turn.config do |c| # :outline - turn's original case/test outline mode [default] c.format = :outline # turn on invoke/execute tracing, enable full backtrace c.trace = true # use humanized test names (works only with :outline format) c.natural = true end #VCR config VCR.config do |c| c.cassette_library_dir = 'spec/fixtures/dish_cassettes' c.stub_with :webmock end
Hay algunas cosas aquí que vale la pena mencionar, así que vamos a analizarlas pieza por pieza:
require_relative
declaración es una adición Ruby 1.9.3. minitest / autorun
Incluye todas las expectativas que usaremos., webmock / minitest
agrega los enlaces necesarios entre las dos bibliotecas, mientras que VCR
y giro
son bastante autoexplicativos. Por último, pero no menos importante, el Rakefile
que contiene algún código de soporte:
require 'rake / testtask' Rake :: TestTask.new do | t | t.test_files = FileList ['spec / lib / dish / * _ spec.rb'] t.verbose = true end task: default =>: test
los rake / testtask
biblioteca incluye un TestTask
Clase que es útil para establecer la ubicación de nuestros archivos de prueba. De ahora en adelante, para ejecutar nuestras especificaciones, solo escribiremos rastrillo
desde el directorio raíz de la biblioteca.
Como una forma de probar nuestra configuración, agreguemos el siguiente código a /lib/dish/player.rb
:
módulo plato clase jugador final fin
Entonces /spec/lib/dish/player_spec.rb
:
require_relative '… /… / spec_helper' # Para Ruby < 1.9.3, use this instead of require_relative # require (File.expand_path('./… /… /… /spec_helper', __FILE__)) describe Dish::Player do it "must work" do "Yay!".must_be_instance_of String end end
Corriendo rastrillo
Debería darte un pase de prueba y ningún error Esta prueba no es de ninguna manera útil para nuestro proyecto, pero verifica implícitamente que nuestra estructura de archivos de biblioteca está en su lugar (el describir
bloque lanzaría un error si el Plato :: Jugador
módulo no fue cargado).
Para funcionar correctamente, Dish requiere los módulos Httparty y la correcta base_uri
, es decir, la URL base de la API de Dribbble. Vamos a escribir las pruebas relevantes para estos requisitos en player_spec.rb
:
… Describa Dish :: El reproductor describe "los atributos predeterminados" debe incluir los métodos httparty "haga Dish :: Player.must_include HTTParty end it" debe tener la url base establecida en el punto final de la Dribble API "do Dish :: Player.base_uri .must_equal 'http://api.dribbble.com' end end end end
Como puede ver, las expectativas de Minitest se explican por sí mismas, especialmente si usted es un usuario de RSpec: la mayor diferencia es la redacción, donde Minitest prefiere "must / wont" a "should / should_not".
La ejecución de estas pruebas mostrará un error y un error. Para que pasen, agreguemos nuestras primeras líneas de código de implementación a jugador.rb
:
módulo Dish class Player incluye HTTParty base_uri 'http://api.dribbble.com' end end
Corriendo rastrillo
De nuevo debería mostrarse las dos especificaciones que pasan. Ahora nuestra Jugador
clase tiene acceso a todos los métodos de clase Httparty, como obtener
o enviar
.
Como estaremos trabajando en el Jugador
Clase, necesitaremos tener datos API para un jugador. La página de documentación de Dribbble API muestra que el punto final para obtener datos sobre un jugador específico es http://api.dribbble.com/players/:id
Como en la moda típica de Rails., :carné de identidad
es o bien el carné de identidad o la nombre de usuario de un jugador específico. Estaremos usando simplebits
, El nombre de usuario de Dan Cederholm, uno de los fundadores de Dribbble..
Para grabar la solicitud con VCR, actualicemos nuestra player_spec.rb
archivo añadiendo lo siguiente describir
Bloque a la especificación, justo después de la primera:
… Describa "GET profile" do antes de hacer VCR.insert_cassette 'player',: record =>: new_episodes finaliza después de que VCR.eject_cassette end it "registra el archivo" do Dish :: Player.get ('/ players / simplebits') fin extremo fin
despues de correr
rastrillo
, Puedes verificar que el aparato ha sido creado. A partir de ahora, todas nuestras pruebas serán completamente independientes de la red..
los antes de
el bloque se utiliza para ejecutar una parte específica del código antes de cada expectativa: lo usamos para agregar la macro de VCR utilizada para grabar un dispositivo que llamaremos "jugador". Esto creará un jugador.yml
archivar bajo spec / fixtures / dish_cassettes
. los :grabar
La opción está configurada para registrar todas las solicitudes nuevas una vez y reproducirlas en cada solicitud subsiguiente e idéntica. Como prueba de concepto, podemos agregar una especificación cuyo único objetivo es grabar un elemento para el perfil de simplebits. los después
La directiva le indica a la VCR que retire el casete después de las pruebas, asegurándose de que todo esté correctamente aislado. los obtener
método en el Jugador
La clase está disponible, gracias a la inclusión del Httparty
módulo.
despues de correr rastrillo
, Puedes verificar que el aparato ha sido creado. A partir de ahora, todas nuestras pruebas serán completamente independientes de la red..
Cada usuario de Dribbble tiene un perfil que contiene una cantidad bastante extensa de datos. Pensemos en cómo nos gustaría que fuera nuestra biblioteca cuando realmente se use: esta es una forma útil de hacer que nuestro DSL funcione. Esto es lo que queremos lograr:
simplebits = Dish :: Player.new ('simplebits') simplebits.profile => # devuelve un hash con todos los datos de la API simplebits.username => 'simplebits' simplebits.id => 1 simplebits.shots_count => 157
Simple y efectivo: queremos crear una instancia de un Jugador utilizando su nombre de usuario y luego obtener acceso a sus datos llamando a los métodos en la instancia que se asignan a los atributos devueltos por la API. Necesitamos ser consistentes con la API misma.
Afrontemos una cosa a la vez y escribamos algunas pruebas relacionadas con la obtención de los datos del jugador desde la API. Podemos modificar nuestra "GET perfil"
bloque para tener:
describe "GET profile" do let (: player) Dish :: Player.new antes de hacer VCR.insert_cassette 'player',: record =>: new_episodes end after after do VCR.eject_cassette end it "debe tener un método de perfil" do player.must_respond_to: profile end it "debe analizar la respuesta api de JSON a Hash" do player.profile.must_be_instance_of Hash end it "debe realizar la solicitud y obtener los datos" do player.profile ["username"]. must_equal 'simplebits 'end end
los dejar
directiva en la parte superior crea una Plato :: Jugador
Instancia disponible en las expectativas. A continuación, queremos asegurarnos de que nuestro jugador tenga un método de perfil cuyo valor sea un hash que representa los datos de la API. Como último paso, probamos una clave de muestra (el nombre de usuario) para asegurarnos de que realmente realizamos la solicitud.
Tenga en cuenta que todavía no estamos manejando cómo configurar el nombre de usuario, ya que este es un paso más. La implementación mínima requerida es la siguiente:
... class Player incluye HTTParty base_uri 'http://api.dribbble.com' def profile self.class.get '/ players / simplebits' end end ...
Una cantidad muy pequeña de código: solo estamos envolviendo una llamada get en el perfil
método. Luego, pasamos la ruta codificada para recuperar los datos de simplebits, datos que ya habíamos almacenado gracias a VCR.
Todas nuestras pruebas deben pasar.
Ahora que tenemos una función de perfil de trabajo, podemos ocuparnos del nombre de usuario. Aquí están las especificaciones relevantes:
describe los "atributos de instancia predeterminados" do let (: player) Dish :: Player.new ('simplebits') it "debe tener un atributo id" do player.must_respond_to: username end it "debe tener la id correcta" do player .username.must_equal 'simplebits' end end describe "GET profile" do let (: player) Dish :: Player.new ('simplebits') antes de hacer VCR.insert_cassette 'base',: record =>: new_episodes termina después do VCR.eject_cassette end it "debe tener un método de perfil" do player.must_respond_to: profile end it "debe analizar la respuesta de api de JSON a Hash" do player.profile.must_be_instance_of Hash end it "debe obtener el perfil correcto" do player .profile ["username"]. must_equal "simplebits" end end
Hemos agregado un nuevo bloque de descripción para verificar el nombre de usuario que vamos a agregar y simplemente enmendamos el jugador
inicialización en el GET perfil
Bloque para reflejar el DSL que queremos tener. Ejecutar las especificaciones ahora revelará muchos errores, como nuestro Jugador
la clase no acepta argumentos cuando se inicializa (por ahora).
La implementación es muy sencilla:
... class Player attr_accessor: username include HTTParty base_uri 'http://api.dribbble.com' def initialize (username) self.username = username end def profile self.class.get "/players/#self.username" end fin…
El método de inicialización obtiene un nombre de usuario que se almacena dentro de la clase gracias a attr_accessor
método añadido anteriormente. Luego cambiamos el método de perfil para interpolar el atributo de nombre de usuario.
Deberíamos conseguir que todas nuestras pruebas pasen una vez más..
En un nivel básico, nuestra lib está en muy buena forma. Como el perfil es un Hash, podríamos detenernos aquí y usarlo al pasar la clave del atributo del cual queremos obtener el valor. Nuestro objetivo, sin embargo, es crear un DSL fácil de usar que tenga un método para cada atributo.
Pensemos en lo que necesitamos lograr. Supongamos que tenemos una instancia de jugador e indicamos cómo funcionaría:
player.username => 'simplebits' player.shots_count => 157 player.foo_attribute => NoMethodError
Vamos a traducir esto a especificaciones y agregarlas a la GET perfil
bloquear:
… Describa "atributos dinámicos" antes de que player.profile end it "debe devolver el valor del atributo si está presente en el perfil" do player.id.must_equal 1 end it "debe aumentar el método si el atributo no está presente" do lambda player. foo_attribute .must_raise NoMethodError end end ...
Ya tenemos una especificación para el nombre de usuario, por lo que no necesitamos agregar otra. Tenga en cuenta algunas cosas:
perfil de jugador
en un bloque anterior, de lo contrario será nulo cuando intentemos obtener el valor del atributo.foo_attribute
genera una excepción, necesitamos envolverla en un lambda y verificar que genere el error esperado.carné de identidad
es igual a 1
, ya que sabemos que ese es el valor esperado (esto es una prueba puramente dependiente de los datos).En cuanto a la implementación, podríamos definir una serie de métodos para acceder a la perfil
hash, sin embargo, esto crearía una gran cantidad de lógica duplicada. Además, confiaría en el resultado de la API para tener siempre las mismas claves..
"Vamos a confiar en
método_missing
para manejar estos casos y 'generar' todos esos métodos sobre la marcha ".
En cambio, confiaremos en método_missing
para manejar estos casos y 'generar' todos esos métodos sobre la marcha. Pero ¿qué significa esto? Sin entrar en demasiada metaprogramación, podemos decir simplemente que cada vez que llamamos a un método que no está presente en el objeto, Ruby genera una NoMethodError
mediante el uso método_missing
. Al redefinir este mismo método dentro de una clase, podemos modificar su comportamiento.
En nuestro caso, interceptaremos el método_missing
llame, verifique que el nombre del método que se ha llamado sea una clave en el hash del perfil y, en caso de resultados positivos, devuelva el valor de hash para esa clave. Si no, llamaremos súper
elevar una norma NoMethodError
: esto es necesario para asegurarnos de que nuestra biblioteca se comporte exactamente como lo haría cualquier otra biblioteca. En otras palabras, queremos garantizar la menor sorpresa posible..
Agreguemos el siguiente código a la Jugador
clase:
def method_missing (name, * args, & block) if profile.has_key? (name.to_s) profile [name.to_s] else end end end
El código hace exactamente lo que se describe arriba. Si ahora ejecuta las especificaciones, debe hacer que todas pasen. Le incito a agregar algunos más a los archivos de especificaciones para algún otro atributo, como disparos_cuenta
.
Esta implementación, sin embargo, no es realmente Ruby idiomática. Funciona, pero se puede convertir en un operador ternario, una forma condensada de condicional if-else. Puede ser reescrito como:
def method_missing (name, * args, & block) profile.has_key? (name.to_s)? perfil [name.to_s]: super end
No es solo una cuestión de longitud, sino también una cuestión de coherencia y convenciones compartidas entre los desarrolladores. Navegar por el código fuente de las gemas y bibliotecas de Ruby es una buena manera de acostumbrarse a estas convenciones.
Como paso final, queremos asegurarnos de que nuestra biblioteca sea eficiente. No debe realizar más solicitudes de las necesarias y, posiblemente, almacenar datos en caché internamente. Una vez más, pensemos en cómo podríamos usarlo:
player.profile => realiza la solicitud y devuelve un Hash player.profile => devuelve el mismo hash player.profile (true) => fuerza la recarga de la solicitud http y luego devuelve el hash (con cambios de datos si es necesario)
¿Cómo podemos probar esto? Podemos usar WebMock para habilitar y deshabilitar las conexiones de red al punto final de la API. Incluso si estamos utilizando dispositivos de VCR, WebMock puede simular un tiempo de espera de red o una respuesta diferente al servidor. En nuestro caso, podemos probar el almacenamiento en caché obteniendo el perfil una vez y luego deshabilitando la red. Llamando perfil de jugador
De nuevo deberíamos ver los mismos datos, mientras que llamando jugador.perfil (verdadero)
deberíamos conseguir un Error de tiempo de espera
, como la biblioteca intentaría conectarse al punto final de la API (deshabilitado).
Añadamos otro bloque a la player_spec.rb
archivo, justo después generación dinámica de atributos
:
describa el "almacenamiento en caché" do # usamos Webmock para deshabilitar la conexión de red después de # recuperar el perfil antes de player.profile stub_request (: any, /api.dribbble.com/).to_timeout finalice "must cache the profile" do player. profile.must_be_instance_of Hash end it "debe actualizar el perfil si está forzado" do lambda player.profile (true) .must_raise Timeout :: Error end end
los solicitud de esbozo
El método intercepta todas las llamadas al punto final de la API y simula un tiempo de espera, aumentando el tiempo esperado. Error de tiempo de espera
. Como hicimos antes, probamos la presencia de este error en un lambda..
La implementación puede ser complicada, así que la dividiremos en dos pasos. En primer lugar, movamos la solicitud http real a un método privado:
... def perfil get_profile end ... privado def get_profile self.class.get ("/ players / # self.username") end ...
Esto no hará que nuestras especificaciones pasen, ya que no estamos guardando en caché el resultado de obtener_perfil
. Para hacer eso, cambiemos la perfil
método:
... def perfil @profile || = get_profile end ...
Almacenaremos el hash de resultado en una variable de instancia. También tenga en cuenta el || =
operador, cuya presencia asegura que obtener_perfil
se ejecuta solo si @profile devuelve un valor falsy (como nulo
).
A continuación podemos añadir la directiva de recarga forzada:
… Def perfil (fuerza = falso) fuerza? @profile = get_profile: @profile || = get_profile end ...
Estamos usando un ternario de nuevo: si fuerza
es falso, realizamos obtener_perfil
y caché, si no, usamos la lógica escrita en la versión anterior de este método (es decir, realizando la solicitud solo si no tenemos un hash).
Nuestras especificaciones deberían ser ecológicas ahora y este es también el final de nuestro tutorial..
Nuestro propósito en este tutorial era escribir una biblioteca pequeña y eficiente para interactuar con la API de Dribbble; Hemos sentado las bases para que esto suceda. La mayor parte de la lógica que hemos escrito puede resumirse y reutilizarse para acceder a todos los demás puntos finales. Minitest, WebMock y VCR han demostrado ser herramientas valiosas para ayudarnos a configurar nuestro código.
.