为新的 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.

继续以下对话:

目标:

  • 针对测试:将 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

主机,例如 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-example.yml 以创建适合您环境的 config.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

那么我们是否还需要这些管道符号?是否保留它们?

好的,那样可以。既然如此,我们就不需要管道了,所以是的,你可以把它们移除。

我目前觉得我们的“测试”部分有点令人困惑,因为它将 discourse_api 相关的任务与 Discourse 本身的安装混合在一起。例如,如果你在云端有一台测试服务器,你完全不需要在本地安装 Discourse。你觉得这样如何:

示例与测试

若要尝试示例或运行测试,你需要一个 已启动并运行的 Discourse 实例,可以安装在本地或云端。

示例

要运行 /examples 目录中的示例:

  1. 通过复制 config-example.yml 创建名为 config.yml 的文件,并在其中指定你的环境设置。
  2. 在某个示例文件中,注释掉除了你想要运行的示例之外的所有内容。
  3. 根据需要编辑示例文件中的参数,例如 username
  4. 运行该文件后,它将从你的 config.yml 中读取配置,并对你的 Discourse 实例执行相应的客户端方法(例如 client.badges)。

测试

要运行 spec 目录中的 discourse_api 测试:

  1. 在 discourse_api 目录下安装 bundler,运行 gem install bundler
  2. 进入你的 discourse_api 目录,运行:bundle exec rspec spec/

听起来不错,README 确实需要更新一下

实际上,运行测试并不需要 Discourse 实例处于运行状态。所有的 spec 请求都应该被模拟,因此不需要服务器。

啊,对了,那么…

示例

要在 /examples 中运行示例,您需要先有一个 正在运行的 Discourse 实例,可以安装在本地或云端,然后:

  1. 通过复制 config-example.yml 来指定您的环境,创建一个名为 config.yml 的文件,并填入您的环境设置。
  2. 在某个示例文件中,注释掉除您想要运行的示例之外的所有内容。
  3. 编辑示例文件,添加所需的参数,例如传递给方法的 username
  4. 运行该文件时,它将读取 config.yml 并针对您的 Discourse 实例执行特定的客户端方法(例如 client.badges)。

测试

要运行 spec 中的 discourse_api 测试:

  1. 在 discourse_api 目录中安装 bundler,运行 gem install bundler
  2. 进入您的 discourse_api 目录,运行:bundle exec rspec spec/