Ruby on Rails API com TDD

Artigos - 17/Fev/2021 - por André Kanamura

Introdução

API é uma solução bastante utilizada atualmente na construção de aplicações Web. É um "interfaceamento" no qual ocorre envio apenas de dados necessários, processamento e resposta e pode ser usada para separar funcionalidades de uma aplicação. Basicamente, ela permite que serviços possam se comunicar sem que um conheça o funcionamento interno do outro, o que simplifica bastante o desenvolvimento.

Ao criar uma aplicação Ruby on Rails, ela já possui toda a estrutura necessária para criar uma API. Existe inclusive uma opção para criar uma app que servirá somente como API, sem expor páginas HTML para o usuário final, por exemplo. Todos esses detalhes estão documentados no Rails Guides. Se preferir, também existe uma versão traduzida para o português aqui

Em resumo: - API é um conjunto de recursos, sendo que cada um realiza uma ação, como criar um item, listar itens, apagar um item. Traduzindo para nossa visão dentro de uma aplicação Ruby on Rails: cada endpoint é uma rota que direciona uma requisição para os controllers da nossa API. - Na criação de APIs, é possível reaproveitar rotas e controllers normalmente usados para as views, mas geralmente temos rotas e controllers separados para tratar os dados de forma independente. - É uma prática comum conter o versionamento dentro da URL de uma API, pois isso ajuda em futuras atualizações. - APIs são muito diferentes umas das outras e dependem muito das necessidades de cada cliente. Mas, em geral, são aplicações compostas por diversas pequenas funções.

Neste tutorial vamos construir uma API Ruby on Rails simples de lista de tarefas e usaremos Ruby 3.0 e a versão 6.1.1 do Rails. Apesar do Rails oferecer uma maneira de gerar uma aplicação especificamente para API, neste artigo utilizamos como base uma aplicação comum, mas os mesmos conceitos podem ser aplicados. Se você estiver criando uma aplicação que é dominantemente uma API, é possível usar o comando rails new my_api --api.

Vamos começar mostrando como faríamos os primeiros testes e, para isso, gostamos de usar a gem rspec-rails: documentação rspec-rails. Além disso, neste tutorial vamos utilizar uma abordagem com desenvolvimento guiado por testes ou TDD (Test-Driven Development).

Estrutura de um teste para API

O teste da API deve ser um teste do tipo Request no Rspec e ele segue a mesma estrutura de sempre: criação dos dados, execução do código e validação de expectativas. Mas as principais diferenças são:

  • o código da API será executado por meio de chamadas HTTP, através dos métodos get, post, delete, patch.
  • as expectativas vão validar o retorno que é atualizado automaticamente na variável response. Vamos testar sempre o response.status e o response.body.

O response.body vai trazer toda resposta vinda via HTTP no formato string. O que podemos fazer em casos de APIs JSON é transformar essa string de volta em um Hash para testar mais detalhes como atributos específicos. Para isso, o Rails já tem a Gem JSON incorporada: https://github.com/flori/json.

Mas antes de qualquer coisa, vamos criar um primeiro teste onde verificamos apenas o status de uma chamada:

require 'rails_helper'

describe 'TasksController', type: :request do
  context 'index' do
    it 'should return status ok' do
      get api_v1_tasks_path

      expect(response).to have_http_status(200)
    end
  end
end

Neste teste queremos apenas criar uma rota index onde serão listadas todas as tarefas cadastradas, mas, neste momento, vamos apenas garantir que ela retornará status 200.

Como criar rotas para cada versão

Note como em nosso teste usamos get '/api/v1/tasks'. Fizemos isso porque vamos separar as rotas de nossa API em versões. Essa prática é importante para que fiquem claramente separadas as lógicas aplicadas em diferentes versões da aplicação e, principalmente, evitar que interfaces API que consomem versões antigas quebrem devido às mudanças. Para que possamos ter uma rota como /api/v1/tasks, podemos utilizar duas estratégias: namespaces ou scopes.

Namespaces

Rails.application.routes.draw do
  namespace 'api' do
    namespace 'v1' do
      resources :tasks, only: [:index]
    end  
  end
end

Scopes

Rails.application.routes.draw do
  scope '/api/v1/' do
    resources :tasks, only: [:index]
  end
end

Tanto Namespaces quanto Scopes cumprem o papel de configurar a URL exatamente do jeito que queremos. No entanto, como pretendemos criar controllers diferentes para versões de API, utilizaremos Namespaces por permitir o uso de módulos para separar as classes.

Como criar o Controller

Com a implementação que fizemos no routes.rb, o controller esperado agora é Api::V1::TasksController. Para refletir isso vamos criar uma nova estrutura de pastas para os controllers da API /app/controllers/api/v1/. Dentro dessa pasta coloque seu novo tasks_controller.rb. Seu novo controller deverá fazer parte de um module do Ruby. Você pode ler mais aqui. A implementação poderia ficar assim:

module Api
  module V1
    class TasksController < Api::V1::ApiController
    end
  end
end

Mas aqui nós vamos seguir o seguinte modelo:

class Api::V1::TasksController < Api::V1::ApiController 
end

Repare que TasksController herda de ApiController, que teria o mesmo papel de um ApplicationController, ou seja, nos dar mais controle sobre o código. A autenticação pode ser um exemplo: podemos ter uma API 100% pública, mas nossa aplicação poderia requerer autenticação de usuários em diferentes rotas.

Então, crie a classe Api::V1::ApiController dentro da pasta /app/controllers/api/v1/ e coloque o conteúdo:

class Api::V1::ApiController < ActionController::API
end

Actions e retorno

As actions podem seguir o mesmo modelo que seguimos no resto do desenvolvimento usual. No nosso exemplo, a action index dentro do controller de tasks poderia ficar assim:

def index
  head 200
end

O método head renderiza somente o head da resposta com o status indicado. Como não queremos renderizar uma view, mas planejamos que a aplicação envie junto com o status todas as tarefas cadastradas, vamos começar a implementar um código que gere uma resposta renderizando um JSON além do status.

def index
  render json: {}, status: 200
end

O código acima fica incompleto, mas passa no teste. Agora vamos complementar nosso teste inicial para continuar a implementação, depois voltaremos neste ponto para finalizá-lo.

Para quem está se habituando com Ruby, encarar JSON como uma Hash é um bom atalho inicial, mas recomendamos ler e entender mais sobre o formato: https://www.w3schools.com/js/js_json_syntax.asp e https://jsonapi.org/.

Renderizando objetos em JSON

O teste

Vamos voltar ao nosso arquivo de testes e criar mais um cenário dentro do contexto de index, dessa vez verificando a listagem de tarefas cadastradas no sistema.

require 'rails_helper'

describe 'TasksController', type: :request do
  context 'index' do
    it 'deve retornar status 200' do
      get api_v1_tasks_path

      expect(response).to have_http_status(200)
    end

    it 'deve retornar todas as tarefas' do
      task = Task.create(title: 'Estudar Ruby',
                      description: 'Assistir aulas da Campus Code')
      other_task = Task.create(title: 'Fazer lição de casa',
                      description: 'Ler apostilas e resolver exercícios')
      get '/api/v1/tasks'

      expect(response).to have_http_status(200)
      expect(response.body).to include task.title
      expect(response.body).to include task.description
      expect(response.body).to include other_task.title
      expect(response.body).to include other_task.description
    end
  end
end

Anteriormente, o teste verificava apenas se response possuía o status HTTP 200. Mas agora ele verifica se o body da resposta inclui o título e a descrição de cada tarefa criada.

Para retornar um objeto em formato JSON dentro da nossa aplicação, vamos utilizar o método json na index em TasksController:

def index
  @tasks = Task.all
  render json: @tasks, status: 200
end 

Além do retorno do JSON, precisamos nos preocupar com o Status Code da resposta HTTP. Até agora não estávamos dando muita atenção para o status, pois o Rails resolve isso por conta própria. Mas agora queremos detalhar um pouco mais. No próprio método render podemos adicionar um status e retornar um dos HTTP Status existentes, por exemplo: render json: @task, status: :ok. No dia a dia, é mais comum testar e renderizar a mensagem de status em vez do código.

Lembre-se dos symbols disponíveis para a maioria dos status code comumente usados:

  :ok
  :not_found
  :created
  :no_content
  :bad_request
  :unauthorized
  :forbidden
  :precondition_failed
  :unprocessable_entity
  :internal_server_error
  :service_unavailable

etc.

Apresentação de objeto

Caso queira apresentar um único objeto ou retornar que um objeto não existe, poderiam ser feitos testes como:

context 'show' do
    it 'deve retornar uma tarefa' do
      task = Task.create(title: 'Estudar Ruby',
                         description: 'Assistir aulas da Campus Code')

      get api_v1_task_path(task)

      expect(response).to have_http_status(200)
      expect(response.body).to include task.title
      expect(response.body).to include task.description
    end

    it 'deve retornar not found se tarefa indisponível' do
      get api_v1_task_path(id:999)

      expect(response).to have_http_status(:not_found)
    end
  end

No arquivo de rotas só seria necessário adicionar a action:

resources :tasks, only: [:index, :show]

E no controller o código poderia ficar mais ou menos assim:

  def show
    @task = Task.find(params[:id])
    render status: 200, json: @task

  rescue ActiveRecord::RecordNotFound
    render status: 404, json: {}
  end

Criação de objetos

Para criação de objetos poderia ser feito um teste como esse:

  context 'post' do
    it 'deve criar tarefa' do
      task = {task: { title: 'Comprar leite',
                      description: "Comprar leite no mercadinho" } }

      post api_v1_tasks_path, params: task

      expect(response).to have_http_status(201)
      expect(response.body).to include('Comprar leite')
      expect(response.body).to include('Comprar leite no mercadinho')
      expect(Task.last.title).to eq('Comprar leite')
    end
  end

No arquivo de rotas só seria necessário adicionar a action:

resources :tasks, only: [:index, :show, :create]

E no controller a action poderia ficar mais ou menos assim:

  def create
    @task = Task.create(task_params)

    render status: 201, json: @task
  end

  private

  def task_params                                                                
    params.require(:task).permit(:title, :description)                           
  end 

Conclusão

Neste artigo mostramos os primeiros passos para começar a testar e criar uma API usando Ruby on Rails. A partir dessa base você já deve ser capaz de criar sua própria API e implementar o código para adequar às suas necessidades.

Se quiser se aprofundar um pouco mais e aprender a usar outras ferramentas como o Faraday ou VCR na sua aplicação, recomendamos a leitura dos nossos artigos abaixo. :)

Leituras adicionais:

Foto de perfil do autor
André Kanamura

Dev na Campus Code