Criando testes para novos endpoints do discourse_api

Adding some polls API endpoints for PR to discourse_api, which work fine. Now I’m trying to understand how to create tests before submitting the PR, e.g.:

require 'spec_helper'

describe DiscourseApi::API::Polls do
  subject { DiscourseApi::Client.new("http://localhost:3000", "test_d7fd0429940", "test_user" )}

  describe "#polls" do
    before do
      stub_get("http://localhost:3000/polls/voters.json").to_return(body: fixture("voters.json"), headers: { content_type: "application/json" })
    end

    it "requests the correct resource" do
      subject.voters :post_id => 27885, :poll_name => 'poll' 
      expect(a_get("http://localhost:3000/polls/voters.json")).to have_been_made
    end
  end
end

But am getting the error:

   Failed to open TCP connection to localhost:3000 (Connection refused - connect(2) for "localhost" port 3000)
 # ./lib/discourse_api/client.rb:141:in `rescue in request'
 # ./lib/discourse_api/client.rb:132:in `request'
 # ./lib/discourse_api/client.rb:85:in `get'
 # ./lib/discourse_api/api/polls.rb:22:in `voters'
 # ./spec/discourse_api/api/polls_spec.rb:12:in `block (3 levels) in <top (required)>'
 # ------------------
 # --- Caused by: ---
 # Errno::ECONNREFUSED:
 #   Connection refused - connect(2) for "localhost" port 3000
 #   /Users/kimardenmiller/.rbenv/versions/2.5.3/gemsets/d_api/gems/webmock-2.3.2/lib/webmock/http_lib_adapters/net_http.rb:109:in `request'

All the existing tests pass fine for me, and I’m trying to copy the format of those existing tests, but cannot for the life of me figure out what I’m doing wrong.

Here’s the (new) endpoint being tested:

module DiscourseApi
  module API
    module Polls
     def poll_voters(args)
        args = API.params(args)    # post_id, poll_name, user, opts = {}
                  .required(:post_id, :poll_name)
                  .optional(:opts, :api_username)
        response = get("/polls/voters.json", args)
        response[:body]
      end
    end
  end
end

Have working tests, though my gut tells me I could use tips on making those urls more elegant.

require 'spec_helper'

describe DiscourseApi::API::Polls do
  subject { DiscourseApi::Client.new("http://localhost:3000", "test_d7fd0429940", "test_user" )}

  describe "#polls" do
    before do
      stub_get("http://localhost:3000/polls/voters.json?post_id=27885&poll_name=poll").to_return(body: fixture("polls_voters.json"), headers: { content_type: "application/json" })
    end

    it "requests the correct resource" do
      subject.poll_voters post_id: 27885, poll_name: 'poll'
      expect(a_get("http://localhost:3000/polls/voters.json?post_id=27885&poll_name=poll")).to have_been_made
    end

    it "returns the expected votes" do
      voters = subject.poll_voters post_id: 27885, poll_name: 'poll'
      expect(voters).to be_a Hash
      voters.each { |g| expect(g).to be_an Array }
      expect(voters['voters']['e539a9df8700d0d05c69356a07b768cf']).to be_an Array
      expect(voters['voters']['e539a9df8700d0d05c69356a07b768cf'][0]['id']).to eq(356)
    end
    
  end
end

Glad you got the tests working. They can be tricky sometimes because if the request doesn’t match exactly it will try and create an actual request rather that using the stubbed request.

I think the urls are okay. It would be nice not to use such a high post_id, but I’m not concerned about that. And ALL the tests need to have http://localhost:3000 extracted out into a common variable, but that should probably be done in a separate commit. Whenever you are ready you can go ahead and submit a pr and I can review it.

One thing I did notice though is I that passing in the api_username as a parameter is no longer supported.

This is because the discourse_api gem now only passes in the auth via the headers, and discourse core ignores any auth credentials in the body if the header is already being used for auth.

Continuando a conversa de

Objetivos:

  • Para Testes: Extrair http://localhost:3000 para uma variável comum

  • Para Exemplos: Extrair host, nome de usuário e api_key para um arquivo config.yml

Estamos decidindo onde colocar e carregar as coisas. No momento, tenho no arquivo discourse_api.rb (com config.yml no diretório raiz):

require 'yaml'
CONFIG = YAML.load_file(File.expand_path('../../config.yml', __FILE__))

… então em cada arquivo de exemplo:

client = DiscourseApi::Client.new(CONFIG['host'])
client.api_key = CONFIG['api_key']
client.api_username = CONFIG['api_username']

Isso funciona para os exemplos, mas não para os testes. Estou interessado em algumas orientações sobre as melhores práticas para este PR.

Vamos dividir os PRs, por favor. Primeiro, vamos focar no PR da especificação e mover a variável comum para a pasta de especificação. Devemos ser capazes de adicioná-la ao arquivo de helper da especificação.

@blake, me avise se tiver algum comentário sobre o projeto acima.

Isso está correto.

Como isso é apenas para exemplos, tudo relacionado a isso deve estar dentro do diretório de exemplos, sem editar nenhuma funcionalidade existente no diretório lib. Até mesmo o arquivo config.yml deve ser colocado dentro do diretório de exemplos. Carregar o arquivo YAML é bastante leve; podemos adicioná-lo no topo de cada arquivo de exemplo, mas provavelmente seria melhor adicioná-lo a um arquivo comum dentro do diretório de exemplos, do qual todos possam ler.

Aqui está o que tenho:

examples/example_helper.rb

$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require File.expand_path('../../lib/discourse_api', __FILE__)
require 'yaml'

CONFIG = YAML.load_file(File.expand_path('../../config.yml', __FILE__))

def client
  discourse_client = DiscourseApi::Client.new(CONFIG['host'])
  discourse_client.api_key = CONFIG['api_key']
  discourse_client.api_username = CONFIG['api_username']
  discourse_client
end

examples/config-example.yml

# host, por exemplo: localhost:3000 ou discourse.meudominio.com
host: SEU_NOME_DE_HOST

# usuário da API (pode afetar os resultados retornados, por exemplo, o método .categories retorna apenas as categorias que seu api_username pode ver)
# crie um novo cliente ao alterar o usuário da API
api_username: SEU_API_KEY

# chave da API do painel de administração do Discourse /admin/api/keys
api_key: SEU_API_KEY

examples/badges.rb

require_relative 'example_helper'

# obter emblemas
puts client.badges

Alguma ideia? Com certeza precisamos do arquivo yml agora, dado o exemplo_helper.rb?

Parece bom. Vamos atualizar o helper de exemplo com valores padrão para que ele funcione sem o arquivo yml. Em seguida, atualize o readme com instruções para copiar o arquivo yml de exemplo para examples/config.yml. E não se esqueça de adicionar examples/config.yml ao .gitignore.

Essa é uma convenção bastante padrão para que não seja fácil cometer um arquivo config.yml com credenciais de produção. E você pode editar o arquivo sem que o git o veja como uma alteração.

Tudo bem, exceto que não entendi muito bem esta parte

O example_helper.rb funciona como está assim que o config.yml existir. Queremos que ele também funcione sem um arquivo yml, mas ainda temos um arquivo yml?

Sim, assim o Ruby não quebra se eles ainda não configuraram o arquivo YAML.

Não exatamente isso, mas algo como:

  discourse_client = DiscourseApi::Client.new(CONFIG['host'] || 'localhost:3000')
  discourse_client.api_key = CONFIG['api_key'] || 'api-key'
  discourse_client.api_username = CONFIG['api_username'] || 'api-username'

Então, antes que o arquivo config.yml seja criado, mesmo com a lógica do duplo pipe, temos um erro:

`initialize': No such file or directory @ rb_sysopen

Para que os pipes sequer entrem em ação, precisaríamos ajustar também o YAML.load_file.

Sim, precisamos ajustar isso também.

Então, isso

config_yml = File.expand_path('../config.yml', __FILE__)

if File.exists? config_yml
  CONFIG = YAML.load_file config_yml
else
  CONFIG = {}
end

def client
  discourse_client = DiscourseApi::Client.new(CONFIG['host'] || 'http://localhost:3000')
  discourse_client.api_key = CONFIG['api_key'] || 'YOUR_API_KEY'
  discourse_client.api_username = CONFIG['api_username'] || 'YOUR_USERNAME'
  discourse_client
end

nos dá erros como

Failed to open TCP connection to localhost:3000 (Connection refused - connect(2) for "localhost" port 3000) (Faraday::ConnectionFailed)

Esse é o erro que preferimos?

Acho que esse erro está correto, pois é assim que os exemplos funcionam atualmente, e isso só ocorre nos exemplos mesmo. Se quiser, agora ou mais tarde, sempre podemos capturar a exceção e fornecer uma mensagem útil.

Talvez …

config_yml = File.expand_path('../configg.yml', __FILE__)

begin
  CONFIG = YAML.load_file config_yml
rescue Errno::ENOENT
  raise ArgumentError, '/examples/config.yml file not found. Please copy config-example.yml to create a config.yml for your environment.'
end

def client
  discourse_client = DiscourseApi::Client.new(CONFIG['host'] || 'http://localhost:3000')
  discourse_client.api_key = CONFIG['api_key'] || 'YOUR_API_KEY'
  discourse_client.api_username = CONFIG['api_username'] || 'YOUR_USERNAME'
  discourse_client
end

Então, ainda precisamos dos pipes? Deixá-los?

Sim, isso funciona. Nesse caso, não precisaremos dos pipes, então sim, você pode removê-los.

A seção de Testes atual está um pouco confusa para mim, pois mistura tarefas relacionadas ao discourse_api com a instalação do próprio Discourse. Por exemplo, se você tiver um servidor de staging na nuvem, não precisará instalar o Discourse localmente. O que você acha disso:

Exemplos e Testes

Para experimentar os exemplos ou executar os testes, você precisará de uma instância do Discourse em funcionamento, instalada localmente ou na nuvem.

Exemplos

Para executar os exemplos em /examples:

  1. Especifique seu ambiente copiando o arquivo config-example.yml e criando um arquivo chamado config.yml com suas configurações de ambiente.
  2. Em um arquivo de exemplo específico, comente todas as linhas, exceto os exemplos que deseja executar.
  3. Edite o arquivo de exemplo com quaisquer parâmetros necessários, por exemplo, username.
  4. Ao executar o arquivo, ele usará seu config.yml para chamar um método de cliente específico (por exemplo, client.badges) na sua instância do Discourse.

Testes

Para executar os testes do discourse_api em spec:

  1. Instale o bundler no diretório do discourse_api executando gem install bundler.
  2. Dentro do diretório do discourse_api, execute: bundle exec rspec spec/.

Parece bom, o README poderia ser atualizado.

Para executar os testes, na verdade, não é necessário ter uma instância do Discourse em execução. Todas as requisições de teste devem ser simuladas (mockadas), e um servidor não é necessário.

Ah, certo, então que tal…

Exemplos

Para testar os exemplos em /examples, você precisará de uma instância do Discourse em execução, instalada localmente ou na nuvem, e então:

  1. Especifique seu ambiente copiando o arquivo config-example.yml para criar um arquivo chamado config.yml com as configurações do seu ambiente.
  2. Em um arquivo de exemplo específico, comente todos os exemplos, exceto aqueles que deseja executar.
  3. Edite o arquivo de exemplo com os parâmetros necessários, por exemplo, um username de destino passado para o método.
  4. A execução do arquivo então usará as informações do seu config.yml para executar um método de cliente específico (por exemplo, client.badges) contra sua instância do Discourse.

Testes

Para executar os testes do discourse_api em spec:

  1. Instale o bundler no diretório discourse_api, executando gem install bundler.
  2. Dentro do diretório discourse_api, execute: bundle exec rspec spec/.