Hey que tal el dia hoy vengo a presentarles un tutorial de como crear un API en Ruby On Rails, sin embargo en este tutorial quiero salirme un poco fuera de la caja de la simple creacion de un CRUD y por lo tanto lo que crearemos lo describo enseguida:
Configuracion de la aplicacion.
Conectarnos a un API externa para obtener elementos por medio de:
- Faraday: Con esta gema crearemos la conexion y llamadas posteriores a el cliente.
- VCR: Aqui gracias a este registraremos las llamadas HTTP a el cliente y utilizaremos cassettes (archivos generados por esta gema en formato yml) para poder crear pruebas de nuestras peticiones.
Pruebas en la aplicacion:
- RSpec: Una de mis gemas favoritas.
El api al que nos conectaremos sera al de pokemon:
Agregaremos el patron de diseño proxy con la finalidad de guardar en cache la consulta al cliente por un periodo de 24 horas.
De igual forma vamos utilizar el Factory Method Pattern para poder crear las respuestas que retornaremos sean exitosas o fallidas. - https://refactoring.guru/design-patterns/factory-method
El repositorio de la aplicacion puede ser encontrado enseguida:
Comentado lo anterior empezaremos por crear nuestra aplicacion, indicando que desamos crear la aplicacion sin el suite de pruebas por defecto y ademas que sera una API con el siguiente comando:
rails new api_project —api -T
Una vez creada la aplicacion procederemos a configurar nuestra aplicacion para poder crear pruebas y empezar a escribir codigo.
Empezaremos por abrir nuestro Gemfile y agregar las siguientes gemas en su ambiente respectivo como se mira enseguida:
group :test do
gem 'vcr', '~> 6.3', '>= 6.3.1'
end
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri windows ]
gem 'rspec-rails', '~> 7.1'
end
# HTTP CLIENT
gem 'faraday', '~> 2.12', '>= 2.12.1'
# Redis - quitar el comentario de esta linea
gem "redis", ">= 4.0.1"
Procederemos a instalar nuestras gemas con un bundle install y posteriormente a configurar RSpec y VCR.
-
Configuracion de RSpec.
Utilizaremos el comando rails generate rspec:install lo cual nos creara una serie de archivos para configurar nuestra suite de pruebas y asi poder empezar a generar pruebas para nuestra aplicacion.
-
Configuracion VCR.
Una vez instalada RSpec procederemos a abrir nuestro archivo rails_helper.rb y justo arriba del bloque de configuracion de RSpec procederemos a agregar el siguiente codigo:
require 'vcr'
VCR.configure do |c|
c.cassette_library_dir = 'spec/vcr_cassettes'
c.hook_into :faraday
c.configure_rspec_metadata!
c.default_cassette_options = {
record: :new_episodes
}
end
- Por ultimo debemos de activar el cache en nuestro ambiente de desarrollo ya que solo esta habilitado en produccion por defecto y esto lo haremos en la terminal ejecutando el siguiente comando:
rails dev:cache
Una vez que terminamos esta configuracion basica es momento de empezar a tirar codigo y esto lo haremos no de la manera convencional de TDD es decir crear pruebas, que pase el codigo y despues refactorizar (red - green -refactor), debido a que en lo personal yo me siento mas comodo creando primero el codigo y despues generando las pruebas (por mi falta de experiencia posiblemente).
Sea como sea siempre debemos crear pruebas para nuestro codigo, en lo personal a mi siempre me gusta tener pruebas para reducir el gap de errores que pueden presentarse una vez que tengamos nuestra aplicacion lista para ser utilizada.
Dicho lo anterior empezaremos por agregar una nueva ruta en nuestra aplicacion dentro del archivo routes.rb como se muestra enseguida:
namespace :api do
namespace :v1 do
get '/pokemon', to: 'pokemons#pokemon'
end
end
Como vemos aqui estamos creando un namespace tanto para api como para la version de esta y esto lo hacemos como buena practica en caso de que en un futuro quisieramos tener una version 2 de la api con nuevas caracteristicas.
Despues procederemos a crear el controlador pokemons dentro de app/controllers/api/v1/pokemons_controller.rb con la siguiente estructura:
module Api
module V1
class PokemonsController < ApplicationController
def pokemon
if params[:pokemon_name].present?
response = get_pokemon(pokemon_name: params[:pokemon_name])
render json: response, status: :ok
else
render json: { 'error' => 'please provide a valid parameter' }, status: :unprocessable_entity
end
end
private
def get_pokemon(pokemon_name:)
::V1::GetPokemonService.new(pokemon_name:).call
end
end
end
end
Aqui estamos creando nuestro metodo pokemon el cual revisa que desde donde hagamos la peticion este presente el parametro pokemon_name al momento de hacer una busqueda y en caso omiso retornaremos un error indicando que el parametro no es valido.
Es decir la unica URI valida sera la siguiente:
http://localhost:3000/api/v1/pokemon?pokemon_name=nombre_de_pokemon
Siguiendo con el flujo de nuestra api estamos mandando a llamar a un metodo privado con el nombre de get_pokemon el cual acepta el parametro pokemon_name.
Este es pasado a una nueva instancia del servicio GePokemonService el cual como es un servicio es llamado directamente con el metodo call.
Esta clase debera de estar dentro del directorio services/v1/get_pokemon_service.rb y tendra la siguiente estructura:
module V1
class GetPokemonService
attr_reader :pokemon_name
def initialize(pokemon_name:)
@pokemon_name = pokemon_name
end
def call
get_pokemon
end
private
def get_pokemon
client = WebServices::PokemonConnection.new
proxy = PokemonProxy.new(client)
proxy.get_pokemon(pokemon_name:)
end
end
end
En este punto llegamos a algo interesante y esto es el uso del patron proxy el cual nos permite tener un sustituto de un objeto para controlar su acceso.
Pero primero lo primero, tenemos una variable llamada client la cual es una instancia de la clase Faraday para conectarnos a el cliente externo con una configuracion especifica que pasamos dentro del un bloque. Esta clase debera de estar dentro del directorio web_services/pokemon_connection.rb
module WebServices
class PokemonConnection
def client(read_timeout = 30)
Faraday.new(url: 'https://pokeapi.co/api/v2/') do |conn|
conn.options.open_timeout = 30
conn.options.read_timeout = read_timeout
conn.request :json
conn.response :logger, nil, { headers: false, bodies: false, errors: false }
conn.response :json
conn.adapter :net_http
end
end
def get_pokemon(pokemon_name:)
response = client.get("pokemon/#{pokemon_name}")
rescue Faraday::Error => e
{ 'error' => e }
end
end
end
Si analizamos un poco el codigo en este metodo hacemos una conexion directa al api mediante la instanciacion de Faraday pasandole como parametro la url a la que nos queremos conectar. Es necesario aclarar que este metodo no se ejecutara hasta que nosotros lo decidamos y en este caso sera mediante la llamada a el metodo get_pokemon cuando hacemos uso del metodo client.get.
Si les interesa conocer mas detalles de Faraday pueden revisar su documentacion en el siguiente vinculo:
https://lostisland.github.io/faraday
El metodo get_pokemon toma el nombre del pokemon y lo que hacemos enseguida es mandar a llamar a client.get(”/pokemon/pikachu”). Por ejemplo en donde el valor de client antes de hacer uso del metodo get sera https://pokeapi.co/api/v2/.
client.get("pokemon/pikachu")
Cuando estamos ejecutando el codigo anterior dentro de nuestro metodo get_pokemon en realidad estamos haciendo un get hacia la siguiente URI:
GET https://pokeapi.co/api/v2/pokemon/pikachu
Si todo es correcto obtendremos una respuesta con toda la informacion del pokemon pikachu y lo podemos probar justo en una ventana de nuestro navegador agregando la direccion https://pokeapi.co/api/v2/pokemon/pikachu con la finalidad de revisar el resultado de lo que nos respondera el API externo.
Acto seguido contamos con nuestro proxy el cual debe de tener la misma interfaz que la clase PokemonConnection esto quiere decir que deberemos de tener el metodo get_pokemon dentro de ella. Su ubicacion sera dentro de app/proxies/pokemon_proxy.rb y tendra el siguiente contenido:
class PokemonProxy
EXPIRATION = 24.hours.freeze
attr_reader :client, :cache
def initialize(client)
@client = client
@cache = Rails.cache
end
def get_pokemon(pokemon_name:)
return pokemon_response('on_cache', cache.read("pokemon_cached/#{pokemon_name}")) if cache.exist?("pokemon_cached/#{pokemon_name}")
response = client.get_pokemon(pokemon_name:)
if response.status == 200
response.body['consulted_at'] = consulted_at
cache.write("pokemon_cached/#{pokemon_name}", response, expires_in: EXPIRATION)
pokemon_response('client_call', response)
else
pokemon_error_response(response)
end
end
private
def consulted_at
Time.now.utc.strftime('%FT%T')
end
def pokemon_response(origin, response)
{
'origin': origin,
'name': response.body['name'],
'weight': response.body['weight'],
'types': type(response.body['types']),
'stats': stats(response.body['stats']),
'consulted_at': response.body['consulted_at']
}
end
def stats(stats)
stats.each_with_object([]) do |stat, array|
array << "#{stat.dig('stat', 'name')}: #{stat['base_stat']}"
end
end
def type(types)
types.map { |type| type.dig('type', 'name') }
end
def pokemon_error_response(response)
{
'error': response.body,
'status': response.status
}
end
end
Desde mi punto de vista esta clase esta haciendo demasiado y tiene mas de una responsabilidad; sin embargo para terminos de enseñanza considero que esta bien ya que al programar tambien debemos de refactorizar y esto lo haremos mas adelante ya que terminemos de crear las pruebas para nuestra API.
Empezaremos por explicar el metodo inicializador este cuenta con un cliente que le pasamos desde su llamada en la clase anterior y ademas agregamos a la variable de instancia @cache inicializando Rails.cache.
# Llamada de clase anterior
client = WebServices::PokemonConnection.new
proxy = PokemonProxy.new(client)
def initialize(client)
@client = client
@cache = Rails.cache
end
Despues tenemos el metodo get_pokemon que como comentamos anteriormente esta clase Proxy debe de tener la misma interfaz que el cliente y la explicaremos enseguida.
return pokemon_response('on_cache', cache.read("pokemon_cached/#{pokemon_name}")) if cache.exist?("pokemon_cached/#{pokemon_name}")
response = client.get_pokemon(pokemon_name:)
if response.status == 200
response.body['consulted_at'] = consulted_at
cache.write("pokemon_cached/#{pokemon_name}", response, expires_in: EXPIRATION)
pokemon_response('client_call', response)
else
pokemon_error_response(response)
end
end
En la primera linea lo que hacemos es retornar un pokemon_response (veremos a continuacion el metodo) siempre y cuando la llave para cache (en este caso “pokemon_cached/pikachu”) exista con los argumentos on_cache y la llave anteriormente mencionada en cache.
En caso de que no exista esa llave para cache lo que hacemos es hacer uso el client que pasamos por parametro al momento de inicializar nuestra instancia y mandamos a llamar al metodo get_pokemon, en donde si el estatus de la respuesta es un 200 entonces a la respuesta que es un hash le agregaremos un nuevo elemento llamado consulted_at que sera la fecha y hora en la que se esta haciendo la consulta al cliente.
Posteriormente procedemos a guardar la respuesta de la llamada al cliente con la llave pokemon_cached/nombre_del_pokemon y ademas pasamos una tiempo de expiracion para esta respuesta con el fin de que durante 24 horas este disponible en cache y asi en futuras llamadas no vayamos a el cliente a preguntarle sobre el pokemon en cuestion.
Enseguida mandamos a llamar a el metodo pokemon_response con los argumentos client_call y la respuesta obtenida por parte del cliente.
En caso de que el estatus de la respuesta del cliente no haya sido favorable (diferente a 200) retornaremos un pokemon_error_response con la respuesta del cliente como argumento.
def consulted_at
Time.now.utc.strftime('%FT%T')
end
def pokemon_response(origin, response)
{
'origin': origin,
'name': response.body['name'],
'weight': response.body['weight'],
'types': type(response.body['types']),
'stats': stats(response.body['stats']),
'consulted_at': response.body['consulted_at']
}
end
def pokemon_error_response(response)
{
'error': response.body,
'status': response.status
}
end
Haciendo referencia a los metodos anteriormente explicados tenemos en primer lugar a consulted_at el cual su unica funcion es brindarnos la fecha y hora cuando es llamado.
pokemon_response cuenta con los parametros origin y response con los cuales construye un objeto del tipo Hash. La primera invocacion a este metodo nos debera de arrojar lo siguiente:
{
:origin => "client_call",
:name => "pikachu",
:weight => 60,
:types => ["electric"],
:stats => [
"hp: 35",
"attack: 55",
"defense: 40",
"special-attack: 50",
"special-defense: 50",
"speed: 90"
],
:consulted_at=>"2024-11-21T02:00:20"
}
En una segunda y futuras invocaciones a este metodo tendremos como resultado lo siguiente:
{
:origin => "on_cache",
:name => "pikachu",
:weight => 60,
:types => ["electric"],
:stats => [
"hp: 35",
"attack: 55",
"defense: 40",
"special-attack: 50",
"special-defense: 50",
"speed: 90"
],
:consulted_at=>"2024-11-21T02:00:20"
}
Mientras que nuestro metodo error_response nos dara el siguiente resultado:
{
"error": "Not Found",
"status": 404
}
Hasta este punto nuestro API ya estara funcional y si lo queremos probar lo podemos hacer de la siguiente manera:
- Iniciamos nuestro servidor de Rails con el comando rails s.
- En otra terminal abriremos una consola de Rails y agregaremos lo siguiente:
require 'net/http'
require 'uri'
url = 'http://localhost:3000/api/v1/pokemon?pokemon_name=pikachu'
uri = URI(url)
response = Net::HTTP.get_response(uri)
response.body
# Resultado
"{\"origin\":\"client_call\",\"name\":\"pikachu\",\"weight\":60,\"types\":[\"electric\"],\"stats\":[\"hp: 35\",\"attack: 55\",\"defense: 40\",\"special-attack: 50\",\"special-defense: 50\",\"speed: 90\"],\"consulted_at\":\"2024-11-21T02:13:03\"}"
Etapa De Pruebas
Para poder generar las pruebas debemos de pensar en la funcionalidad que actualmente tenemos y que nos da como resultado. Por lo tanto si analizamos nuestro codigo contamos con los siguientes escenarios:
1.- Tendremos un caso exitoso cuando el nombre del pokemon sea valido:
- La primera peticion a este endpoint debe de traernos el origin como client_call.
- La segunda peticion a este endpoint debe de traernos el origin como on_cache.
2.- Tendremos un caso fallido cuando:
- El nombre del pokemon sea invalido.
- El parametro pokemon_name no este presente como query params.
Basado en esto que conocemos de nuestra aplicacion procederemos a crear la siguiente prueba dentro de spec/requests/pokemons_spec.rb:
require 'rails_helper'
RSpec.describe Api::V1::PokemonsController, type: :request, vcr: { record: :new_episodes } do
let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) }
let(:pokemon_name) { 'pikachu' }
before do
allow(Rails).to receive(:cache).and_return(memory_store)
end
describe 'GET /api/v1/pokemon' do
context 'with a valid pokemon' do
it 'returns data from the client on the first request and caches it for subsequent requests' do
# first call - fetch data from the client
get "/api/v1/pokemon?pokemon_name=#{pokemon_name}"
expect(response.status).to eq(200)
pokemon = parse_response(response.body)
expect(pokemon['origin']).to eq('client_call')
pokemon_information(pokemon)
expect(Rails.cache.exist?("pokemon_cached/#{pokemon_name}")).to eq(true)
# second call - fetch data from cache
get "/api/v1/pokemon?pokemon_name=#{pokemon_name}"
pokemon = parse_response(response.body)
expect(pokemon['origin']).to eq('on_cache')
pokemon_information(pokemon)
end
end
context 'with an invalid pokemon' do
it 'returns an error' do
get '/api/v1/pokemon?pokemon_name=unknown_pokemon'
error = parse_response(response.body)
expect(error['error']).to eq('Not Found')
expect(error['status']).to eq(404)
end
end
context 'with invalid parameters' do
it 'returns an error' do
get '/api/v1/pokemon?pokemon_namess=unknown_pokemon'
error = parse_response(response.body)
expect(error['error']).to eq('Invalid Parameters')
end
end
end
# Helper methods
def pokemon_information(pokemon)
expect(pokemon['name']).to eq('pikachu')
expect(pokemon['weight']).to eq(60)
expect(pokemon['types']).to eq(['electric'])
expect(pokemon['stats']).to eq([
'hp: 35',
'attack: 55',
'defense: 40',
'special-attack: 50',
'special-defense: 50',
'speed: 90'
])
expect(pokemon['consulted_at']).to be_present
end
def parse_response(response)
JSON.parse(response)
end
end
En la primera linea nos encontraremos con el uso de VCR en donde solo se utilizara para las peticiones que hagamos a el cliente de pokemon en donde esperamos una respuesta exitosa o fallida segun como nos responda este.
Despues empezamos por crear dos lets los cuales se explican enseguida:
- memory_store: Esta creara una instancia de cache.
- pokemon_name: Definimos el nombre del pokemon que utilizaremos para mantener nuestro codigo DRY.
Enseguida contamos con un bloque before en donde unicamente stubeamos Rails.cache con la finalidad de que nos retorne una instancia de cache.
Y ahora si empezamos a probar nuestro endpoint que creamos con el siguiente bloque de codigo:
it 'returns data from the client on the first request and caches it for subsequent requests' do
# first call - fetch data from the client
get "/api/v1/pokemon?pokemon_name=#{pokemon_name}"
expect(response.status).to eq(200)
pokemon = parse_response(response.body)
expect(pokemon['origin']).to eq('client_call')
pokemon_information(pokemon)
expect(Rails.cache.exist?("pokemon_cached/#{pokemon_name}")).to eq(true)
# second call - fetch data from cache
get "/api/v1/pokemon?pokemon_name=#{pokemon_name}"
pokemon = parse_response(response.body)
expect(pokemon['origin']).to eq('on_cache')
pokemon_information(pokemon)
end
Lo que hacemos en esta prueba es bien sencillo:
- Mandamos a llamar a nuestro endpoint pokemon en donde le pasamos el query param pokemon_name con el nombre del pokemon (pikachu).
- Esperamos que la respuesta obtenida sea un 200.
- Parseamos el body de la respuesta obtenida.
- El campo de nuestro json con el valor de origin debera de ser igual a client_call.
- Posterior a esto validamos que el json retornado sea justo como nosotros lo deseamos, es decir:
def pokemon_information(pokemon)
expect(pokemon['name']).to eq('pikachu')
expect(pokemon['weight']).to eq(60)
expect(pokemon['types']).to eq(['electric'])
expect(pokemon['stats']).to eq([
'hp: 35',
'attack: 55',
'defense: 40',
'special-attack: 50',
'special-defense: 50',
'speed: 90'
])
expect(pokemon['consulted_at']).to be_present
end
- Enseguida esperamos que se haya almacenado la respuesta obtenida con la llave pokemon_cached/pikachu.
- Volvemos a llamar a nuestro endpoint pokemon justo con las mismas caracteristicas de la primera peticion.
- Ahora esperamos que el campo de nuestro json con el valor de origin sea igual a on_cache.
- Por ultimo validamos de nuevo que el json retornado cumpla con las caracteristicas necesarias.
Ahora vamos con los casos fallidos y el primero de estos nos retornara un 404 debido a que no se encontro el pokemon que le pasamos por parametro, no hay mucho que hablar esta es una prueba muy sencilla y esto es lo que esperamos.
context 'with an invalid pokemon' do
it 'returns an error' do
get '/api/v1/pokemon?pokemon_name=unknown_pokemon'
error = parse_response(response.body)
expect(error['error']).to eq('Not Found')
expect(error['status']).to eq(404)
end
end
Posteriomente contamos con el ultimo caso que valida que si el parametro es invalido nos retorne un error en especifico.
context 'with invalid parameters' do
it 'returns an error' do
get '/api/v1/pokemon?pokemon_namess=unknown_pokemon'
error = parse_response(response.body)
expect(error['error']).to eq('Invalid Parameters')
end
end
Lo ultimo que tenemos es un par de helper method para evitar estar repitiendo codigo en nuestra spec.
Ahora procederemos a ejecutar nuestras pruebas con bundle exec rspec y esto nos dara como resultado que nuestras pruebas pasaran sin problema.
Ademas de esto se crearan unos archivos en formato YAML los cuales son los registros guardados dentro de spec/vcr_cassettes de las peticiones que hagamos al cliente de pokemon y estos nos permitiran si asi lo deseamos usar estos mismos para mas pruebas que deseemos hacer en un futuro con la respuesta obtenida de parte del cliente.
Una vez que hayamos terminado de ejecutar nuestraas pruebas sera necesario cambiar el hash del simbolo vcr de new_episodes a none como se muestra enseguida.
RSpec.describe Api::V1::PokemonsController, type: :request, vcr: { record: :none }
Refactorizando
Hay especificamente dos partes que considero debemos de refactorizar la primera de ellas es el controlador de la siguiente manera:
module Api
module V1
class PokemonsController < ApplicationController
def pokemon
if params[:pokemon_name].present?
response = get_pokemon(pokemon_name: params[:pokemon_name])
render json: response.pokemon_body, status: response.status
else
render json: { 'error': 'Invalid Parameters' }, status: :unprocessable_entity
end
end
private
def get_pokemon(pokemon_name:)
::V1::GetPokemonService.new(pokemon_name:).call
end
end
end
end
Si nos damos cuenta dentro de nuestra respuesta al servicio GetPokemon estamos accediendo a pokemon_body y status de esta manera nos olvidamos de hardcodear en especifido el status de la peticion que le hagamos al cliente.
Pues bien para que esto funcione deberemos de aplicar una segunda refactorizacion en el proxy que hemos creado anteriormente de la siguiente manera:
class PokemonProxy
EXPIRATION = 24.hours.freeze
attr_reader :client, :cache
def initialize(client)
@client = client
@cache = Rails.cache
end
def get_pokemon(pokemon_name:)
return WebServices::FactoryResponse.create_response(origin: 'on_cache', response: cache.read("pokemon_cached/#{pokemon_name}"), type: 'success') if cache.exist?("pokemon_cached/#{pokemon_name}")
response = client.get_pokemon(pokemon_name:)
if response.status == 200
response.body['consulted_at'] = consulted_at
cache.write("pokemon_cached/#{pokemon_name}", response, expires_in: EXPIRATION)
WebServices::FactoryResponse.create_response(origin: 'client_call', response:, type: 'success')
else
WebServices::FactoryResponse.create_response(origin: 'client_call', response:, type: 'failed')
end
end
private
def consulted_at
Time.now.utc.strftime('%FT%T')
end
end
Anteriormente contabamos con metodos para retornar una respuesta exitosa o fallida dependiente de como nos respondia el cliente, sin embargo ahora estamos haciendo uso del patron de diseño Factory Method el cual practicamente lo utilizaremos para crear objetos (respuesta) basados en el tipo que le pasamos como argumentos.
Dicho lo anterior primero crearemos nuestro FactoryResponse con el siguiente contenido:
module WebServices
class FactoryResponse
def self.create_response(origin:, response:, type:)
case type
when 'success'
PokemonResponse.new(origin:, response:)
when 'failed'
PokemonFailedResponse.new(response:)
end
end
end
end
Basado en el tipo que recibe esta clase mediante el metodo de clase create_response determinaremos el tipo de respuesta que debemos de retornar, en donde si es un caso exitoso retornaremos un PokemonResponse con el siguiente contenido:
module WebServices
class PokemonResponse
attr_reader :origin, :response
def initialize(origin:, response:)
@origin = origin
@response = response
@pokemon_body = pokemon_body
end
def pokemon_body
{
'origin': origin,
'name': response.body['name'],
'weight': response.body['weight'],
'types': type(response.body['types']),
'stats': stats(response.body['stats']),
'consulted_at': response.body['consulted_at']
}
end
def status
response.status
end
private
def stats(stats)
stats.each_with_object([]) do |stat, array|
array << "#{stat.dig('stat', 'name')}: #{stat['base_stat']}"
end
end
def type(types)
types = types.map { |type| type.dig('type', 'name') }
end
end
end
En caso contrario lo que retornaremos un PokemonFailedResponse con el siguiente contenido:
module WebServices
class PokemonFailedResponse
attr_reader :response
def initialize(response:)
@response = response
@pokemon_body = pokemon_body
end
def pokemon_body
{
'error': response.body,
'status': response.status
}
end
def status
response.status
end
end
end
Con esto logramos que seguir el principio S de SOLID (Single Responability) y de esta manera podemos modificar en un futuro la respuesta exitosa o fallida en su clase correspondiente.
Ahora bien si ejecutamos bundle exec rspec nuestras pruebas deberan de pasar sin ningun problema y abremos terminado con la creacion del proyecto.
Por ultimo espero que este pequeño proyecto les haya gustado y si tienen alguna duda o comentario por favor haganmelo saber y con todo gusto estare ahi para responderles, buen dia a ti que estas leyendo y happy coding.
Top comments (0)