TDD com Ruby on Rails e Faraday

Tutoriais - 23/Abr/2020 - por André Kanamura

Neste tutorial vamos apresentar o processo de desenvolvimento com testes utilizando como base uma aplicação Ruby on Rails simples que acessa a Covid19 Brazil API para obter dados de casos da Covid-19 nos estados brasileiros. Utilizaremos Ruby 2.7 e Rails 6.0.

Para acessar os dados da API, precisaremos fazer requisições HTTP e, para lidar com elas, utilizaremos a gem Faraday, uma ferramenta que nos oferece grande praticidade. Você pode ver mais sobre esta gem aqui.

Além disso, vamos usar Rspec puro com stubs para fazer nossos testes. No entanto, vale lembrar que é possível utilizar VCR para melhorar a confiabilidade dos nossos testes, evitando usar stubs, que podem não corresponder ao retorno real da API. Na área de desenvolvimento web existe uma discussão em torno de quando usar stubs ou mocks. Não entraremos nesta discussão aqui, mas você pode ler um pouco sobre isso no artigo do Martin Fowler: Mocks Aren't Stubs.

Vamos começar pelo nosso teste de feature, verificando a apresentação dos dados para cada estado na home:

feature 'Visitor views home page' do
  scenario 'successfully' do
    data_states = [StateInfo.new(state_id: 11,
                                uf: "Rondônia",
                                abbreviation: "RO",
                                cases: 1,
                                deaths: 0,
                                suspicions: 61),
                   StateInfo.new(state_id: 12,
                                uf: "Acre",
                                abbreviation: "AC",
                                cases: 2,
                                deaths: 1,
                                suspicions: 12)]
    allow(StateInfo).to receive(:all).and_return(data_states)

    visit root_path

    expect(page).to have_css('h1', text: "Covid-19 Pandemic Agregator")
    expect(page).to have_content('Rondônia')
    expect(page).to have_content('RO')
    expect(page).to have_content('Casos: 1')
    expect(page).to have_content('Mortes: 0')
    expect(page).to have_content('Suspeitas: 61')
    expect(page).to have_content('Acre')
    expect(page).to have_content('AC')
    expect(page).to have_content('Casos: 2')
    expect(page).to have_content('Mortes: 1')
    expect(page).to have_content('Suspeitas: 12')
  end
end

Criamos um array com estados – neste caso apenas 2 – e os dados que queremos apresentar. Para simular os dados recebidos da API vamos utilizar um PORO (Plain Old Ruby Object) e fazer um stub para que StateInfo retorne nosso data_states quando for chamado o método all na classe. Com isso, esperamos poder chamar StateInfo.all e receber um array com todos os dados dos estados e poderemos apresentá-los na home.

Nossos testes unitários vão garantir que o método all está recebendo os dados no formato enviado pela API e retornando um array no formato que esperamos. Além disso, aproveitamos para lidar com o possível erro da API não responder corretamente:

describe StateInfo do
  it 'should get all states information' do
    json = File.read(Rails.root.join('spec/support/brazil_states_data.json'))
    url = 'https://covid19-brazil-api.now.sh/api/report/v1'
    response = double('faraday_response', body: json, status: 200)
    allow(Faraday).to receive(:get).with(url).and_return(response)
    result = StateInfo.all

    expect(result.length).to eq 3
    expect(result[0].uf).to eq 'Rondônia'
    expect(result[0].abbreviation).to eq 'RO'
    expect(result[0].cases).to eq 0
    expect(result[0].deaths).to eq 0
    expect(result[0].suspicions).to eq 61
    expect(result[1].uf).to eq 'Acre'
    expect(result[2].uf).to eq 'Amazonas'
  end

  it 'should return return empty array  if API error' do
    url = 'https://covid19-brazil-api.now.sh/api/report/v1'
    response = double('faraday_response', body: "{}", status: 500)
    allow(Faraday).to receive(:get).with(url).and_return(response)

    result = StateInfo.all
    expect(result.length).to eq 0
  end
end

Para simular os dados da API vamos novamente fazer um stub. Note que utilizaremos o Faraday para montar nossa resposta da requisição HTTP, por isso o teste fica assim: allow(Faraday).to receive(:get).with(url).and_return(echo). Além disso, utilizamos um arquivo de texto externo para diminuir a quantidade de linhas do teste. Assim, criamos o arquivo brazil_states_data.json com o seguinte:

{
  "data": [
    {
    "uid": 11,
    "uf": "Rondônia",
    "state": "RO",
    "cases": 0,
    "deaths": 0,
    "suspects": 61,
    "refuses": 2,
    "broadcast": false,
    "comments": "",
    "datetime": "2020-03-18T23:00:00.000Z"
    },
    {
    "uid": 12,
    "uf": "Acre",
    "state": "AC",
    "cases": 0,
    "deaths": 0,
    "suspects": 12,
    "refuses": 0,
    "broadcast": false,
    "comments": "",
    "datetime": "2020-03-18T23:00:00.000Z"
    },
    {
    "uid": 13,
    "uf": "Amazonas",
    "state": "AM",
    "cases": 1,
    "deaths": 0,
    "suspects": 18,
    "refuses": 26,
    "broadcast": false,
    "comments": "",
    "datetime": "2020-03-18T23:00:00.000Z"
    }
   ]
}

Depois disso, implementamos nossa classe StateInfo com os métodos initialize e all:

class StateInfo
  attr_accessor :state_id, :uf, :abbreviation, :cases, :deaths, :suspicions
  def initialize(state_id:, uf:, abbreviation:, cases:, deaths:, suspicions:)
    @state_id = state_id
    @uf = uf
    @abbreviation = abbreviation
    @cases = cases
    @deaths = deaths
    @suspicions = suspicions
  end

  def self.all
    response = Faraday.get('https://covid19-brazil-api.now.sh/api/report/v1')

    return {} unless response.status == 200

    json = JSON.parse(response.body, symbolize_names: true)[:data]
    result = json.map do |state|
      state = new(state_id: state[:uid],uf: state[:state], abbreviation: state[:uf],
                      cases: state[:cases], deaths: state[:deaths],
                      suspicions: state[:suspects])
    end
    result
  end
end

No initialize podemos incluir os parâmetros que acharmos mais importantes. Agora sim, dentro do método all, usamos Faraday.get para criar a resposta que queremos a partir da requisição usando o endereço da API: 'https://covid19-brazil-api.now.sh/api/report/v1'.

Nesse instante, podemos receber a resposta e tratar os dados, utilizando-os para montar um array com os dados de cada estado. Ao final, o método all retornará esse array com as instâncias de StateInfo, que será utilizado para exibir as informações na view.

Esse é apenas um exemplo simplificado – do teste até a implementação – de como você pode usar o Faraday para tratar requisições HTTP em uma aplicação Ruby on Rails. Essa ferramenta possui muitas outras funções. Consulte a documentação oficial da gem para aprender como utilizá-las para as situações específicas da sua aplicação.

Referências

Foto de perfil do autor
André Kanamura

Dev na Campus Code