Blog > Ruby in Tests (Parte 3): Testes em APIs com RSpec e RestClient

22/fev

Este post objetiva-se a apresentar conceitos e mostrar na prática como automatizar testes em APIs Rest usando dois frameworks Ruby de código aberto. Falaremos sobre como usar a robustez do RestClient para interagir com APIs em conjunto com o RSpec, framework usado para criar testes automatizados. Ao fim da leitura, você será capaz de criar seus primeiros testes de APIs para valida-las sob a perspectiva funcional, não funcional e estrutural escrevendo em Ruby.

Pré-requisitos

O RSpec, framework discutido no primeiro post desta série, aqui servirá como base para os scripts que construiremos. Assim, antes de prosseguir na leiture deste post recomendo a leitura e execução dos exemplos contidos em Ruby in Tests (Parte 1): Testes de Unidade com RSpec.

Introdução

A evolução da arquitetura dos softwares trouxe uma série de benefícios para o desenvolvimento de uma aplicação, um deles foi a utilização de webservices, que viabilizaram a criação da inteligência da aplicação em um lugar em comum, disponível na internet. Separando assim, interface e core da aplicação. Isso fez com que fosse possível contruir muitas interfaces diferentes para um mesmo aplicativo.

Testes em APIs são realizados normalmente sem a utilização de uma interface gráfica, isso faz com que os testes fiquem um pouco mais complexos em relação a testes em aplicações que possuem interface gráfica. Além disso, APIs são criadas com o intuito de fornecer comunicação entre máquina e máquina, logo, a leitura de uma resposta de uma API não é tão simples.

Automatizar esses testes torna-se então algo necessário e de grande valia, e por isso, este, que é o 3º post da série Ruby In Tests, vem apresentar como utilizar o melhor da arquitetura de testes em Ruby em conjunto com um excelente framework de interação com APIs.

Preparando o projeto

  1. Crie um diretório chamado "API" dentro do diretório "Ruby in Tests"
  2. Abra o arquivo Gemfile contido no diretório "Ruby in Tests"
  3. Adicione o comando a seguir abaixo a linha que contém gem 'rspec', '~> 3.3.0': gem 'rest-client', '~> 1.8'
  4. Adicione o comando a seguir logo abaixo da linha que acaba de ser adicionada: gem 'json-schema', '~> 2.6'
  5. Execute o comando a seguir no prompt de comando: bundler update

Qualister-API

Como trata-se de um post voltado a testes de API, usaremos a Qualister-API, ela possui poucos métodos mas já será o suficiente para atingir nosso objetivo. Bem, ela possui os seguintes resources e métodos:

# Resource Método Parâmetros Retorno
1 / GET nome: String Dados institucionais da Qualister (JSON)
2 /curso GET modo: Enum['parcial', 'total'] Cursos fornecidos pela Qualister
3 /curso POST cursonome: String,
cursodescricao: String,
cursocargahoraria: String
Dados do curso que foram adicionados, contendo também o ID

Construindo o primeiro teste

Para utilizar o RestClient em seus testes, abra o arquivo "env.rb" (contido no diretório Ruby In Tests) e adicione em uma nova linha o comando abaixo:

require 'rest-client'

Agora iremos criar um novo arquivo no diretório "API", chamado "informacoes_cursos_spec.rb". Este arquivo terá os testes que iremos fazer em nossa API. A primeira linha deste arquivo será a chamada ao arquivo "env.rb" onde estão as requisições às bibliotecas que iremos utilizar, veja:

require_relative '../env.rb'

A estrutura do teste continuará sendo criada a partir do RSpec, então iremos adicionar as linhas abaixo ao nosso arquivo:

describe "Gerenciando Informações e Cursos" do
  context "/" do
    context "GET" do
      it "Validar dados da Qualister" do
        # Arrange
        # Act
        # Assert 
      end
    end
  end
end

A organização do teste fica ao critério do testador que o escreve, mas este é um no qual tenho usado e creio ser bastante organizado, segue o formato: Escopo, Resource, Método e Teste. Onde "Escopo" representa quais áreas serão testadas; "Resource" representa o módulo que será testado; "Método" representa qual método será testado (poderia ser POST, PUT, DELETE, etc); e por fim, o teste que será executado neste método.

Dentro do teste, aqui representado pelo "it", vemos a mesma estrutura AAA usada no primeiro prost desta série. Aqui isto também ajuda a estruturar melhor os testes, deixando-os mais objetivos.

Uma vez que já temos a estrutura, vamos começar a testar usando o RestClient em conjunto com o RSpec, o primeiro passo será confirgurar a API que iremos testar, veja:

# Arrange
api = RestClient::Resource.new('http://qualister.info/qualister-api')

Veja que a variável "api" foi criada, ela armazenará um objeto do tipo Resource, que basicamente nos dará ferramentas para interagir com a API da Qualister. Ele espera que seja passado um parâmetro, que corresponde à URL onde está a nossa aplicação. Ao executar este teste temos a seguinte resposta:

Gerenciando Informaçoes e Cursos
  /
    GET
      Validar dados da Qualister (FAILED - 1)
Failures:
  1) Gerenciando Informaçoes e Cursos / GET Validar dados da Qualister
     Failure/Error: api.get(url: "/", params: { nome: "Julio" }),
     RestClient::Unauthorized:
       401 Unauthorized
     # ./API/informacoes_cursos_spec.rb:12:in `block (4 levels) in <top (require
d)>'
Finished in 0.052 seconds (files took 0.87 seconds to load)
1 example, 1 failure

O código 401 mostra que esta API é protegida por usuário e senha, logo, precisaremos também adicionar estas informações na configuração da API, veja:

# Arrange
api = RestClient::Resource.new('http://qualister.info/qualister-api', user: 'qualister', password: 'qualister')

E agora sim, ao executar novamente nosso teste, vemos a seguinte mensagem:

Gerenciando Informaçoes e Cursos
  /
    GET
      Validar dados da Qualister
Finished in 0.003 seconds (files took 0.966 seconds to load)
1 example, 0 failures

Continuemos a construir nosso teste, o segundo passo é fazer a requisição a um resource, utilizando algum método específico. APIs Rest possuem uma série de métodos, e cada um deles tem um objetivo específico, dentre eles temos: GET, POST, PUT, DELETE, etc. Todos eles são suportados pelo RestCliente, logo, podemos utilizar todos se necessário. Vamos fazer a chamada ao resource 1 da tabela que vimos no tópico anterior, veja:

# Act
resposta = api["/"].get(params: { nome: "Julio de Lima" })

A variável Resposta utiliza o método "get" (também poderia ser post, put, delete, etc.) de nossa API, que faz chamada ao resource "/", que segundo a tabela contida no tópico anterior, espera receber um parâmetro "nome". Como vemos, estamos passando o valor "Julio de Lima" para o parâmetro "nome". Logo, se neste momento adicionarmos o código abaixo ao nosso script, conseguiremos ver a resposta à requisição enviada, que será um JSON, veja:

puts resposta.body

Veja o que obtivemos de resposta:

{
  "status":"success",
  "message":"Julio, seja bem-vindo a API da Qualister",
  "data":
  { 
    "atuacao": ["Treinamentos","Consultoria","Revenda de ferramentas"],
    "consultores": 
    {
      "quantidade":5,
      "nomes": ["Cristiano Caetano","Elias Nogueira","Julio de Lima","Roberto Ungarelli"]
    }
  }
}

Este é o JSON da resposta, mas como é um JSON, seria difícil capturar os valores contidos nele tranatndo-o como string, por isso precisaremos convertê-lo, veja abaixo:

# puts resposta.body
corpo    = JSON.parse(resposta.body, object_class: OpenStruct)

A variável Corpo recebendo o método "parse" do objeto "JSON". Este método espera que passemos apenas um parâmetro, que é a string JSON que desejamos converter em objeto. Por isso, passamos o "body" da "resposta" que obtivemos. Simples, não? O que ocorre é que, quando passamos apenas um parâmetro, o valor retornado teria de ser mapeado, e isso tornaria este post menos didático, por isso, ao invés de passar apenas um parâmetro, estamos passando dois, onde o segundo parâmetro faz com que a conversão crie um objeto estruturado (OpenStruct). Desse modo, poderemos ter acesso ao corpo da resposta usando códigos como o apresentado abaixo, veja:

# corpo.status
# corpo.data.atuacao
# corpo.data.consultores.nomes[2]

Agora podemos validar se os dados obtidos foram realmente os esperados, faremos isso através do uso de expectativas. Você aprendeu a utilizar expectativas no primeiro post desta série. Vamos lá, nossa primeira expectativa será para avaliar o status e a mensagem de sucesso, veja como faremos:

# Assert
expect(corpo.status).to eq("success")
expect(corpo.message).to eq("Julio de Lima, seja bem-vindo a API da Qualister")

Agora, ao executar o teste, veremos que ele tem sucesso e então recebemos a mensnagem a seguir:

Gerenciando Informaçoes e Cursos
  /
    GET
      Validar dados da Qualister
Finished in 0.053 seconds (files took 0.864 seconds to load)
1 example, 0 failures

Vamos ver o que acontece quando mudamos o parâmetro de envio (contido na sessão "Act") para "Julio" ao invés de "Julio de Lima":

# Act
resposta = api["/"].get(params: { nome: "Julio" })
corpo    = JSON.parse(resposta.body, object_class: OpenStruct)

A resposta da execução do teste, será:

Gerenciando Informaçoes e Cursos
  /
    GET
      Validar dados da Qualister (FAILED - 1)
Failures:
  1) Gerenciando Informaçoes e Cursos / GET Validar dados da Qualister
     Failure/Error: expect(corpo.message).to eq("Julio de Lima, seja bem-vindo a
 API da Qualister")
       expected: "Julio de Lima, seja bem-vindo a API da Qualister"
            got: "Julio, seja bem-vindo a API da Qualister"
       (compared using ==)
     # ./API/informacoes_cursos_spec.rb:17:in `block (4 levels) in <top (required)>'

O próprio teste nos avisa que o teste falhou, porque esperava "Julio de Lima" e recebeu apenas "Julio". Retorne o parâmetro para "Julio de Lima" e então rode novamente o teste para ver que continua passando.

Temos, duas propriedades no corpo da resposta que são listas, são elas: data.atuacao e data.consultores.nomes. Podemos fazer validações nelas utilizando matchers do RSpec, criados espeficiamente para isso, veja:

expect(corpo.data.atuacao).to include("Consultoria")
expect(corpo.data.consultores.nomes.length).to eq(4)

Testando performance de um método

Para este tópico, usaremos como base o segundo resource da tabela contida no tópic "Qualister-api" deste post, logo, criaremos a seguinte estrutura de teste após o contexto do resource "/":

context "/" do 
  [...]
end
context "/curso" do
  context "GET" do
    it "Validar o tempo da resposta da exibição parcial de cursos" do
      # Arrange
      # Act
      # Assert
    end
    it "Validar o tempo da resposta da exibição total de cursos" do
      # Arrange
      # Act
      # Assert
    end
  end
end

Vamos lá, a performance pode ser medida de forma geral, por exemplo, todas as respostas devem ser recebeidas em até 8 segundos, ou de forma particular, por exemplo, esta resposta deve ser recebido em até 5 segundos. Veja como fazer com que todas as requisições tenham o tempo limite de 8 segundos:

# Arrange
api = RestClient::Resource.new('http://qualister.info/qualister-api', user: 'qualister', password: 'qualister', timeout: 8)

Perceba que agora estamos utilizando o parâmetro "timeout" na configuração da API, esse restringe a devolução da respota de todos os métodos em até 8 segundos. Se alguma requisição feita através de "api" tomar mais que 8 segundos para receber resposta uma exceção do tipo RestClient::RequestTimeout será lançada, fazendo com que o teste falhe. Se desejarmos configurar o timeout para cada requisição, basta adicionar o timeout na requisição. Para demonstrar isso, iremos criar um novo contexto e um novo "it", agora para avaliar o funcionamento do resource "curso", que apresenta parcialmente os cursos da Qualister, veja:

# Act
resposta = api["/curso"].get(params: { modo: "parcial" }, timeout: 5)

A pesquisa parcial leva menos que 5 segundos para ser executada, por isso, ao executar o teste, temos a resposta a seguir:

Gerenciando Informaçoes e Cursos
  /
    GET
      Validar dados da Qualister
  /curso
    GET
      Validar o tempo da resposta da exibiçao parcial de cursos
Finished in 0.092 seconds (files took 0.898 seconds to load)
2 examples, 0 failures

Agora, vamos fazer uma requisição ao mesmo resource, com o mesmo método, mais trocando o parâmetro "parcial" por "total" para vermos qual resposta teremos, veja:

it "Validar o tempo da resposta da exibição parcial de cursos" do
  [...]
end
it "Validar o tempo da resposta da exibição total de cursos" do
  # Arrange
  api = RestClient::Resource.new('http://qualister.info/qualister-api', user: 'qualister', password: 'qualister', timeout: 8)
  # Act
  resposta = api["/curso"].get(params: { modo: "total" }, timeout: 5)
  # Assert
end

Veja que agora estamos fazendo a requisição usando o parâmetro "modo" com o valor "total". Este parâmetro trará muitos cursos, assim, demorará mais que 5 segundos, e ao executarmos este teste ele resultará na mensagem a seguir, veja:

Gerenciando Informaçoes e Cursos
  /
    GET
      Validar dados da Qualister
  /curso
    GET
      Validar o tempo da resposta da exibiçao parcial de cursos
      Validar o tempo da resposta da exibiçao total de cursos (FAILED - 1)
Failures:
  1) Gerenciando Informaçoes e Cursos /curso GET Validar o tempo da resposta da
exibiçao total de cursos
     Failure/Error: resposta = api["/curso"].get(params: { modo: "total" }, time
out: 5)
     RestClient::RequestTimeout:
       Request Timeout
     # ./API/informacoes_cursos_spec.rb:39:in `block (4 levels) in <top (require
d)>'
     # ------------------
     # --- Caused by: ---
     # IO::EWOULDBLOCKWaitReadable:
     #   A non-blocking socket operation could not be completed immediately. - r
ead would block
     #   ./API/informacoes_cursos_spec.rb:39:in `block (4 levels) in <top (requi
red)>'
Finished in 16.1 seconds (files took 0.953 seconds to load)
3 examples, 1 failure
Failed examples:
rspec ./API/informacoes_cursos_spec.rb:34 # Gerenciando Informaçoes e Cursos /cu
rso GET Validar o tempo da resposta da exibiçao total de cursos

Assim, vemos que é possível controlar o tempo limite para resposta, de forma geral, ou mesmo por cada um dos métodos. Apenas para não continuar recebendo esse erro, altere totos os timeouts do teste 10 segundos.

Refatorando utilizando Hooks

Provavelmente você já percebeu que a linha abaixo está se repetindo em todos os "it"s:

api = RestClient::Resource.new('http://qualister.info/qualister-api', user: 'qualister', password: 'qualister', timeout: 10)

Para melhorarmos esse código, o ideal é fazer com que esta linha esteja dentro de um hook "before", logo abaixo do describe "Gerenciando Informações e Cursos", veja a implementação:

describe "Gerenciando Informações e Cursos" do
  before do
    @api = RestClient::Resource.new('http://qualister.info/qualister-api', user: 'qualister', password: 'qualister', timeout: 10)
  end
  [...]
end

O próximo passo é apagar a linha da criação da variável "api", contida na sessão "Arrange", em todos os "it"s:

context "/" do
  context "GET" do
    it "Validar dados da Qualister" do
      # Act
      resposta = api.get(url: "/", params: { nome: "Julio de Lima" })
      # puts resposta.body
      corpo    = JSON.parse(resposta.body, object_class: OpenStruct)
      # Assert
      expect(corpo.status).to eq("success")
      expect(corpo.message).to eq("Julio de Lima, seja bem-vindo a API da Qualister")
      expect(corpo.data.atuacao).to include("Consultoria")
      expect(corpo.data.consultores.nomes.length).to eq(4)
    end
  end
  end
  context "/curso" do
  context "GET" do
    it "Validar o tempo da resposta da exibição parcial de cursos" do
      # Act
      resposta = api["/curso"].get(params: { modo: "parcial" }, timeout: 10)
    end
    it "Validar o tempo da resposta da exibição total de cursos" do
      # Act
      resposta = api["/curso"].get(params: { modo: "total" }, timeout: 10)
    end
  end
end

E, por fim, precisamos colocar um @ antes de cada variável "api", pois elas agora são referencias ao "api" declarado no Hook "before", veja como fica:

context "/" do
  context "GET" do
    it "Validar dados da Qualister" do
      # Act
      resposta = @api.get(url: "/", params: { nome: "Julio de Lima" })
      # puts resposta.body
      corpo    = JSON.parse(resposta.body, object_class: OpenStruct)
      # Assert
      expect(corpo.status).to eq("success")
      expect(corpo.message).to eq("Julio de Lima, seja bem-vindo a API da Qualister")
      expect(corpo.data.atuacao).to include("Consultoria")
      expect(corpo.data.consultores.nomes.length).to eq(4)
    end
  end
  end
  context "/curso" do
  context "GET" do
    it "Validar o tempo da resposta da exibição parcial de cursos" do
      # Act
      resposta = @api["/curso"].get(params: { modo: "parcial" }, timeout: 10)
    end
    it "Validar o tempo da resposta da exibição total de cursos" do
      # Act
      resposta = @api["/curso"].get(params: { modo: "total" }, timeout: 10)
    end
  end
end

Ótimo, agora nossos testes estão muto mais simples, legíveis e melhor, executáveis:

Gerenciando Informaçoes e Cursos
  /
    GET
      Validar dados da Qualister
  /curso
    GET
      Validar o tempo da resposta da exibiçao parcial de cursos
      Validar o tempo da resposta da exibiçao total de cursos
Finished in 8.11 seconds (files took 0.994 seconds to load)
3 examples, 0 failures

Testando método Post

Veremos agora como fazer o teste do último método da tabela contido no tópico "Qualister-API", veremos que é muito semelhante aos demais testes. Criaremos um novo contexto dentro do contexto "/curso", que é o contexto "POST". Lembre-se que os contextos são apenas descrições e não interferem em nada na forma de execução dos testes, veja:

context "/curso" do
  context "GET" do
    [...]
  end
  context "POST" do
    it "Adicionando um novo curso" do
      # Act
      resposta = @api["/curso"].post(
        params: { 
          cursonome: "RSpec e RestClient para APIs",
          cursodescricao: "Este curso apresenta conceitos sobre automação de testes em APIs",
          cursocargahoraria: "8 horas" 
        }, 
        timeout: 10
      )
      corpo    = JSON.parse(resposta.body, object_class: OpenStruct)
      # Assert
      expect(corpo.status).to eq("success")
      expect(corpo.message).to eq("Curso adicionado com sucesso")
    end
  end
end

Ao executar este teste teremos a resposta a seguir, veja:

Gerenciando Informaçoes e Cursos
  /
    GET
      Validar dados da Qualister
  /curso
    GET
      Validar o tempo da resposta da exibiçao parcial de cursos
      Validar o tempo da resposta da exibiçao total de cursos
    POST
      Adicionando um novo curso
Finished in 8.13 seconds (files took 0.879 seconds to load)
4 examples, 0 failures

Validando o esquema da resposta

O site json-schema.org fornece um framework de diretrizes de como validar o formato dos dados de um arquivo JSON. Iremos basear-se nele para validar o formato das respostas das respostas obtidas a partir de nossa API.

O foco deste teste será apenas o primeiro resource contido na tabela do tópico "Qualister-API".

Para começar, adicione a linha abaixo no arquivo "env.rb" contido no diretório "Ruby In Tests":

require 'json-schema'

Agora dicionaremos um novo contexto dentro do contexto do resource "/", este validará o esquema da resposta do método "GET", veja:

context "/" do
  context "GET" do
    [...]
  end
  context "SCHEMA" do 
    it "Validar esquema da resposta" do
      # Act
      resposta = @api["/"].get(params: { nome: "Julio de Lima" })
      # Assert
      expect(JSON::Validator.validate!('API/esquemas/esquema-modelo.json', resposta.body)).to be_truthy
    end
  end
end

Perceba que, basicamente, o que fazemos é enviar uma requisição ao resource "/", usando o método "GET" e então utilizamos o corpo da resposta (que é em formato JSON) para compará-lo com o nosso esquema modelo, escrito utilizando o formato json-schema. É este esquema modelo que dita as regras de validação do esquema da resposta. O método "validate!" do objeto JSON::Validator retorna "true" ou "false" e dispara uma exceção quando o resultado for false. Então, para validar, basta adiciona uma expectativa para validar se o método retorno "true". Veja abaixo o esquema modelo que estamos utilizando, neste caso, salvamos o esquema dentro de um diretório "esquemas" que criamos no diretório "API", veja:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "Resource Principal Qualister API",
  "type": "object",
  "properties": {
    "status": {
      "type": "string"
    },
    "message": {
      "type": "string"
    },
    "data": {
      "type": "object",
      "properties": {
        "atuacao": {
          "type": "array"
        },
        "consultores": {
          "type": "object",
          "properties": {
            "quantidade": {
              "type": "integer"
            },
            "nomes": {
              "type": "array"
            }
          }
        }
      }
    }
  },
  "required": ["status", "message"]
}

Perceba que através do esquema modelo é possível validar quais são os tipos de cada uma das propriedades, além de validar quais são propriedades obrigatórias. Por exemplo, se o arquivo esquema-modelo.json, tivesse a propridade "status" como sendo do tipo "integer", ao executar o erro abaixo seria lançado:

JSON::Schema::ValidationError:
       The property '#/status' of type String did not match the following type:
integer

Retorne o status para o tipo "string" e agora adicione mais um valor na categoria "required", por exemplo, "teste", ficando assim:

  "required": ["status", "message", "teste"]

E então, ao executar o teste, teremos o seguinte erro:

JSON::Schema::ValidationError:
  The property '#/' did not contain a required property of 'teste'

Remova a categoria "teste", e então o teste voltará a passar.

Existem vários tipos de validações que podem ser utilizadas, confira no site json-schema.org.

Versão final do código

require_relative '../env.rb'

describe "Gerenciando Informações e Cursos" do
  before do
	@api = RestClient::Resource.new('http://qualister.info/qualister-api', user: 'qualister', password: 'qualister', timeout: 10)
  end
  
  context "/" do
	context "GET" do
		it "Validar dados da Qualister" do
			# Act
			resposta = @api["/"].get(params: { nome: "Julio de Lima" })
			# puts resposta.body
			corpo    = JSON.parse(resposta.body, object_class: OpenStruct)
			
			# Assert
			expect(corpo.status).to eq("success")
			expect(corpo.message).to eq("Julio de Lima, seja bem-vindo a API da Qualister")
			expect(corpo.data.atuacao).to include("Consultoria")
			expect(corpo.data.consultores.nomes.length).to eq(4)
		end
	end
	
	context "SCHEMA" do 
		it "Validar esquema da resposta" do
			# Act
			resposta = @api["/"].get(params: { nome: "Julio de Lima" })
	
			# Assert
			expect(JSON::Validator.validate!('API/esquemas/esquema-modelo.json', resposta.body)).to be_truthy
		end
	end
  end
  
  context "/curso" do
	context "GET" do
		it "Validar o tempo da resposta da exibição parcial de cursos" do
			# Act
			resposta = @api["/curso"].get(params: { modo: "parcial" }, timeout: 10)
		end
		
		it "Validar o tempo da resposta da exibição total de cursos" do
			# Act
			resposta = @api["/curso"].get(params: { modo: "total" }, timeout: 10)
		end
	end
	
	context "POST" do
		it "Adicionando um novo curso" do
			# Act
			resposta = @api["/curso"].post(
				params: { 
					cursonome: "RSpec e RestClient para APIs",
					cursodescricao: "Este curso apresenta conceitos sobre automação de testes em APIs",
					cursocargahoraria: "8 horas" 
				}, 
				timeout: 10
			)
			corpo    = JSON.parse(resposta.body, object_class: OpenStruct)
			
			
			# Assert
			expect(corpo.status).to eq("success")
			expect(corpo.message).to eq("Curso adicionado com sucesso")
		end
	end
  end
end

Relatório de execução em HTML

Para gerar um relatório de execução dos testes no formato HTML basta adicionar os parâmetros --format html e --out arquivo_html.html, veja um exemplo:

rspec API\informacoes_cursos_spec.rb --format html --out relatorio_api.html

A execução deste comando não traz retorno no prompt de comando, ele apenas gera o arquivo de relatório, semelhante a imagem abaixo:

Conclusão

Vimos que é possível validar uma API Rest de maneira bem simples sobre diversas características, entre elas: funcional, não funcional e estrutural. A utilização do framework RSpec auxilia muito na estruturação e validação dos testes, de modo que, sem ele, tornaria-se muito mais complexo ter resultados menos produtivos.

Links interessantes

  1. rspec.info
  2. github.com/rest-client/rest-client
  3. github.com/ruby-json-schema/json-schema
  4. json-schema.org

POSTS RELACIONADOS

AGENDA

CURSOS RELACIONADOS