Tests für neue discourse_api-Endpunkte erstellen

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.

Fortsetzung der Diskussion aus

Ziele:

  • Für Tests: http://localhost:3000 in eine gemeinsame Variable auslagern

  • Für Beispiele: Host, Benutzername und api_key in eine config.yml-Datei auslagern

Wir entscheiden gerade, wo Dinge platziert und geladen werden sollen. Momentan habe ich dies in der Datei discourse_api.rb (mit config.yml im Root-Verzeichnis):

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

… dann in jeder Beispieldatei:

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

Das funktioniert für die Beispiele, aber nicht für die Tests. Ich würde gerne einige Hinweise zu bewährten Praktiken für diesen PR erhalten.

Lass uns die PRs bitte aufteilen. Konzentrieren wir uns zunächst auf die Spezifikation und verschieben wir die gemeinsame Variable in den Spezifikationsordner. Wir sollten in der Lage sein, sie in die Spec-Helferdatei aufzunehmen.

@blake lass mich wissen, ob du Anmerkungen zum obigen Design hast.

Das ist in Ordnung.

Da dies nur für Beispiele gedacht ist, sollten alle damit verbundenen Dinge im examples-Verzeichnis liegen und keine bestehende Funktionalität im lib-Verzeichnis verändern. Selbst die config.yml-Datei sollten wir im examples-Verzeichnis platzieren. Das Laden der YAML-Datei ist sehr leichtgewichtig, und wir können sie oben in jede Beispiel-Datei einfügen, aber es wäre wahrscheinlich besser, sie in eine gemeinsame Datei im examples-Verzeichnis zu legen, auf die alle zugreifen können.

Hier ist, was ich habe:

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, z. B. localhost:3000 oder discourse.my_domain.com
host: YOUR_HOST_NAME

# API-Benutzer (kann die zurückgegebenen Ergebnisse beeinflussen, z. B. gibt die Methode .categories nur die Kategorien zurück, die für deinen api_username sichtbar sind)
# Erstelle einen neuen Client, wenn du den API-Benutzer änderst
api_username: YOUR_API_KEY

# API-Schlüssel aus dem Discourse-Admin-Bereich /admin/api/keys
api_key: YOUR_API_KEY

examples/badges.rb

require_relative 'example_helper'

# Badges abrufen
puts client.badges

Meinungen? Brauchen wir die YML-Datei jetzt wirklich, da wir example_helper.rb verwenden?

Sieht gut aus. Aktualisieren wir den Beispiel-Helper mit Standardwerten, damit er ohne die YAML-Datei funktioniert. Aktualisieren Sie dann die README mit Anweisungen, die Beispiel-YAML-Datei nach examples/config.yml zu kopieren. Vergessen Sie nicht, examples/config.yml zur .gitignore hinzuzufügen.

Dies ist eine gängige Konvention, um zu vermeiden, dass versehentlich eine config.yml-Datei mit Produktionszugangsdaten committet wird. So können Sie die Datei bearbeiten, ohne dass Git dies als Änderung erkennt.

Alles in Ordnung, außer dass ich diesen Punkt nicht ganz verstehe.

example_helper.rb funktioniert so, wie es ist, sobald config.yml existiert. Wir möchten, dass es auch ohne eine YML-Datei funktioniert, aber wir haben immer noch eine YML-Datei?

Ja, so explodiert Ruby nicht, falls die YAML-Datei noch nicht eingerichtet wurde.

Nicht genau so, aber etwas wie:

  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'

Also, bevor die Datei config.yml erstellt wird, explodieren wir selbst mit der Logik für doppelte Pipes mit

`initialize': No such file or directory @ rb_sysopen

Damit die Pipes überhaupt zum Einsatz kommen, müssten wir auch YAML.load_file anpassen.

Ja, das müssen wir ebenfalls anpassen.

Also, dieser Code:

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

liefert uns Fehler wie:

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

Ist das der gewünschte Fehler?

Ich denke, dieser Fehler ist in Ordnung, da die Beispiele derzeit so funktionieren, und das betrifft ohnehin nur die Beispiele. Wenn du möchtest, können wir jetzt oder später die Ausnahme abfangen und eine hilfreiche Meldung ausgeben.

Vielleicht …

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

Brauchen wir die Pipes dann noch? Sollen wir sie drinlassen?

Ja, das funktioniert. In diesem Fall brauchen wir die Rohre nicht, also ja, du kannst sie entfernen.

Der aktuelle Abschnitt „Testing

Klingt gut, die README könnte etwas aktualisiert werden

Um die Tests auszuführen, benötigst du tatsächlich keine laufende Discourse-Instanz. Alle Spec-Anfragen sollten gemockt werden, und ein Server ist nicht erforderlich.

Ah, richtig, also wie wäre es mit …

Beispiele

Um die Beispiele in /examples auszuprobieren, benötigen Sie eine laufende Discourse-Instanz, die entweder lokal oder in der Cloud installiert ist. Gehen Sie dann wie folgt vor:

  1. Legen Sie Ihre Umgebung fest, indem Sie eine Kopie von config-example.yml erstellen und daraus eine Datei namens config.yml mit Ihren Umgebungs-Einstellungen anlegen.
  2. Kommentieren Sie in einer bestimmten Beispieldatei alle Beispiele außer denjenigen aus, die Sie ausführen möchten.
  3. Bearbeiten Sie die Beispieldatei mit den erforderlichen Parametern, z. B. einen Ziel-username, der an die Methode übergeben wird.
  4. Beim Ausführen der Datei werden die Einstellungen aus Ihrer config.yml verwendet, um eine bestimmte Client-Methode (z. B. client.badges) gegen Ihre Discourse-Instanz auszuführen.

Tests

Um die Discourse-API-Tests in spec auszuführen:

  1. Installieren Sie Bundler im Verzeichnis discourse_api mit dem Befehl gem install bundler.
  2. Führen Sie im Verzeichnis discourse_api folgenden Befehl aus: bundle exec rspec spec/