Building Tests for New discourse_api Endpoints

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.

다음 대화의 이어서 진행합니다.

목표:

  • 테스트용: http://localhost:3000을 공통 변수로 추출

  • 예제용: host, username, api_key를 config.yml 파일로 추출

현재 무엇을 어디에 배치하고 로드할지 결정하는 중입니다. 현재 discourse_api.rb 파일에 (루트 디렉토리에 config.yml 포함) 다음과 같이 설정되어 있습니다:

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

… 그리고 각 예제 파일에서는:

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

이 방식은 예제에서는 작동하지만 테스트에서는 작동하지 않습니다. 이 PR에 대한 모범 사례에 대한 조언을 듣고 싶습니다.

PR을 나눠서 처리해 주세요. 먼저 스펙 관련 PR에 집중하고, 공용 변수를 스펙 폴더로 이동합시다. 스펙 헬퍼 파일에 추가할 수 있어야 합니다.

@blake 위 설계에 대해 의견이 있으시면 말씀해 주세요.

이것은 괜찮습니다.

이것은 예시용이므로, 이 관련 내용은 examples 디렉토리 안에 두어야 하며 lib 디렉토리 안의 기존 기능에 영향을 주어서는 안 됩니다. config.yml 파일도 examples 디렉토리 안에 배치해야 합니다. yaml 파일을 로드하는 것은 매우 가볍고 각 예시 파일 상단에 추가할 수 있지만, examples 디렉토리 안의 공통 파일에 추가하여 모든 예시 파일에서 이를 읽을 수 있도록 하는 것이 더 나을 것입니다.

현재 제가 가지고 있는 코드는 다음과 같습니다:

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 예: localhost:3000 또는 discourse.my_domain.com
host: YOUR_HOST_NAME

# api 사용자 (결과에 영향을 줄 수 있음, 예: .categories 메서드는 api_username이 볼 수 있는 카테고리만 반환)
# api 사용자를 변경할 경우 새로운 클라이언트를 생성하십시오
api_username: YOUR_API_KEY

# Discourse 관리 패널 /admin/api/keys에서 가져온 api 키
api_key: YOUR_API_KEY

examples/badges.rb

require_relative 'example_helper'

# 배지 가져오기
puts client.badges

생각해 주실 수 있을까요? example_helper.rb가 있는 상황에서 이제 yml 파일이 필요한가요?

좋아 보입니다. 예제 헬퍼에 기본값을 추가해서 yml 파일 없이도 작동하도록 업데이트합시다. 그런 다음 readme에 예제 yml 파일을 examples/config.yml로 복사하는 방법을 설명하는 지침을 추가하세요. 그리고 examples/config.yml을 .gitignore에 반드시 추가하세요.

이것은 프로덕션 자격 증명이 포함된 config.yml 파일을 실수로 커밋하지 않도록 하는 꽤 표준적인 관행입니다. 또한 파일을 수정해도 git이 변경사항으로 인식하지 않습니다.

다만, 이 부분은 좀 이해가 안 되네요

config.yml 파일이 존재하면 example_helper.rb는 현재 상태 그대로 작동합니다. yml 파일 없이도 작동하게 하려는 건데, 그럼에도 yml 파일은 계속 유지되는 건가요?

네, 이렇게 하면 yaml 파일을 아직 설정하지 않은 경우 ruby가 에러를 일으키지 않습니다.

정확히 이렇지는 않지만, 다음과 같은 형태가 될 것입니다:

  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'

즉, config.yml 파일이 생성되기 전에는 이중 파이프 로직을 사용해도 다음과 같은 오류가 발생합니다.

`initialize': No such file or directory @ rb_sysopen

파이프 로직이 실제로 적용되려면 YAML.load_file도 조정해야 합니다.

네, 그 부분도 함께 조정해야 합니다.

즉, 이 코드

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

을 사용하면 다음과 같은 오류가 발생합니다.

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

이것이 우리가 원하는 오류 메시지인가요?

현재 예제들이 그렇게 작동하고, 예제에서만 발생하는 문제이므로 이 에러는 괜찮다고 생각합니다. 원하시면 지금이나 나중에 예외를 잡아서 도움이 되는 메시지를 제공할 수 있습니다.

아마도 …

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

begin
  CONFIG = YAML.load_file config_yml
rescue Errno::ENOENT
  raise ArgumentError, '/examples/config.yml 파일을 찾을 수 없습니다. 환경에 맞는 config.yml을 생성하려면 config-example.yml을 복사하세요.'
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

그럼에도 불구하고 파이프를 여전히 필요로 하나요? 그대로 두어야 하나요?

네, 그 방법이 작동하네요. 그렇다면 파이프가 필요 없으니 삭제해도 됩니다.

현재의 Testing 섹션이 다소 혼란스럽습니다. discourse_api 관련 작업과 Discourse 자체의 설치가 뒤섞여 있기 때문입니다. 예를 들어, 클라우드에 스테이징 서버가 있는 경우 로컬에 Discourse를 설치할 필요가 전혀 없습니다. 다음 제안에 대해 어떻게 생각하시나요?

예제 및 테스트

예제를 실행하거나 테스트를 수행하려면 로컬 또는 클라우드에 설치된 실행 중인 Discourse 인스턴스가 필요합니다.

예제

/examples 디렉토리의 예제를 실행하려면:

  1. config-example.yml을 복사하여 config.yml이라는 이름의 파일을 만들고, 여기에 환경 설정을 입력합니다.
  2. 실행하려는 예제만 남기고 해당 예제 파일 내의 다른 모든 예제를 주석 처리합니다.
  3. 필요한 매개변수(예: username)로 예제 파일을 편집합니다.
  4. 파일을 실행하면 config.yml에서 설정을 가져와 주어진 클라이언트 메서드(예: client.badges)를 Discourse 인스턴스에 대해 실행합니다.

테스트

spec에서 discourse_api 테스트를 실행하려면:

  1. discourse_api 디렉토리에서 bundler를 설치합니다. gem install bundler를 실행하세요.
  2. discourse_api 디렉토리 내에서 다음을 실행합니다: bundle exec rspec spec/

좋네요, README를 업데이트해야 할 것 같습니다

테스트를 실행하려면 실제로 Discourse 인스턴스가 실행 중일 필요가 없습니다. 모든 스펙 요청은 목(mock) 처리되어야 하며 서버는 필요하지 않습니다.

아, 맞다. 그러면 …

예제

/examples의 예제를 실행하려면 Discourse 인스턴스가 구동 중이어야 하며, 로컬 또는 클라우드에 설치된 후 다음을 수행합니다:

  1. 환경 설정을 지정하려면 config-example.yml을 복사하여 환경 설정이 담긴 config.yml 파일을 만듭니다.
  2. 주어진 예제 파일에서 실행하려는 예제 외의 모든 예제를 주석 처리합니다.
  3. 필요한 매개변수(예: 메서드에 전달할 대상 username 등)로 예제 파일을 편집합니다.
  4. 파일을 실행하면 config.yml에서 값을 가져와 주어진 클라이언트 메서드(예: client.badges)를 Discourse 인스턴스에 대해 실행합니다.

테스트

spec에서 discourse_api 테스트를 실행하려면:

  1. discourse_api 디렉터리에서 bundler를 설치합니다. gem install bundler를 실행합니다.
  2. discourse_api 디렉터리 내에서 다음을 실행합니다: bundle exec rspec spec/