Как проверить, отправляет ли рабочий процесс Sidekiq правильные данные во внешний API?

У меня есть рабочий Sidekiq, который обращается к внешнему API, чтобы вернуть некоторые данные. Я пытаюсь написать тесты, чтобы убедиться, что этот рабочий спроектирован и работает правильно. Рабочий процесс захватывает локальный экземпляр модели и проверяет два поля в модели. Если одно из полей равно nil, оно отправит поле другое в удаленный API.

Вот рабочий код:

class TokenizeAndVectorizeWorker
  include Sidekiq::Worker
  sidekiq_options queue: 'tokenizer_vectorizer', retry: true, backtrace: true

  def perform(article_id)
    article = Article.find(article_id)
    tokenizer_url = ENV['TOKENIZER_URL']

    if article.content.nil?
      send_content = article.abstract
    else
      send_content = article.content
    end

    # configure Faraday
    conn = Faraday.new(tokenizer_url) do |c|
      c.use Faraday::Response::RaiseError
      c.headers['Content-Type'] = 'application/x-www-form-urlencoded'
    end

    # get the response from the tokenizer
    resp = conn.post '/tokenize', "content=#{URI.encode(send_content)}"

    # the response's body contains the JSON for the tokenized and vectorized article content
    article.token_vector = resp.body

    article.save
  end
end

Я хочу написать тест, чтобы убедиться, что если содержание статьи равно нулю, то реферат статьи отправляется для кодирования.

Мое предположение состоит в том, что «правильный» способ сделать это - имитировать ответы с помощью Фарадея, так что я ожидаю определенного ответа на определенный ввод. Создав статью с содержанием nil и абстрактным x, я могу смоделировать ответ на отправку x в удаленный API и смоделировать ответ на отправку nil в удаленный API. Я также могу создать статью с x в качестве резюме и z в качестве содержания и имитировать ответы для z.

Я написал тест, который обычно издевается над Фарадеем:

    it "should fetch the token vector on ingest" do
      # don't wait for async sidekiq job
      Sidekiq::Testing.inline!

      # stub Faraday to return something without making a real request
      allow_any_instance_of(Faraday::Connection).to receive(:post).and_return(
        double('response', status: 200, body: "some data")
      )

      # create an attrs to hand to ingest
      attrs = {
        data_source: @data_source,
        title: Faker::Book.title,
        url: Faker::Internet.url,
        content: Faker::Lorem.paragraphs(number: 5).join("<br>"),
        abstract: Faker::Book.genre,
        published_on: DateTime.now,
        created_at: DateTime.now
      }

      # ingest an article from the attrs
      status = Article.ingest(attrs)

      # the ingest occurs roughly simultaneously to the submission to the
      # worker so we need to re-fetch the article by the id because at that
      # point it will have gotten the vector saved to the DB
      @token_vector_article = Article.find(status[1].id)

      # we should've saved "some data" as the token_vector
      expect(@token_vector_article.token_vector).not_to eq(nil)
      expect(@token_vector_article.token_vector).to eq("some data")
    end

Но это издевается над 100% использованием Фарадея с :post. В моем конкретном случае я понятия не имею, как имитировать ответ :post с конкретным телом...

Также возможно, что я собираюсь протестировать все это неправильно. Вместо этого я мог бы проверить, что мы отправляем правильный контент (тест должен проверять, что отправляется с помощью Фарадея), и полностью игнорировать правильный ответ.

Каков правильный способ проверить, что этот рабочий делает правильно (отправляет контент или отправляет аннотацию, если контент равен нулю)? Это проверка того, что отправляется, или проверка того, что мы получаем в качестве отражения того, что отправляется?

Если я должен проверять, что возвращается как отражение того, что отправляется, как мне имитировать разные ответы от Фарадея в зависимости от значения чего-то, что ему отправляется/

** примечание добавлено позже **

Я еще немного покопался и подумал: «Хорошо, позвольте мне проверить, что я отправляю ожидаемый запрос и правильно обрабатываю ответ». Итак, я попытался использовать webmock.

    it "should fetch token vector for article content when content is not nil" do
      require 'webmock/rspec'
      # don't wait for async sidekiq job
      Sidekiq::Testing.inline!

      request_url = "#{ENV['TOKENIZER_URL']}/tokenize"

      # webmock the expected request and response
      stub = stub_request(:post, request_url)
             .with(body: 'content=y')
             .to_return(body: 'y')

      # create an attrs to hand to ingest
      attrs = {
        data_source: @data_source,
        title: Faker::Book.title,
        url: Faker::Internet.url,
        content: "y",
        abstract: Faker::Book.genre,
        published_on: DateTime.now,
        created_at: DateTime.now
      }

      # ingest an article from the attrs
      status = Article.ingest(attrs)

      # the ingest occurs roughly simultaneously to the submission to the
      # worker so we need to re-fetch the article by the id because at that
      # point it will have gotten the vector saved to the DB
      @token_vector_article = Article.find(status[1].id)

      # we should have sent a request with content=y
      expect(stub).to have_been_requested

      # we should've saved "y" as the token_vector
      expect(@token_vector_article.token_vector).not_to eq(nil)
      expect(@token_vector_article.token_vector).to eq("y")
    end

Но я думаю, что веб-макет не используется в работе sidekiq, потому что я получаю это:

1) Article tokenization and vectorization should fetch token vector for article content when content is not nil
     Failure/Error: expect(stub).to have_been_requested

       The request POST https://zzzzz/tokenize with body "content=y" was expected to execute 1 time but it executed 0 times

       The following requests were made:

       No requests were made.
       ============================================================

Если я попытаюсь включить webmock/rspec в любом другом месте, например, в начале моего файла, случайные вещи начнут взрываться. Например, если у меня есть эти строки в начале этого файла спецификации:

require 'spec_helper'
require 'rails_helper'
require 'sidekiq/testing'
require 'webmock/rspec'

Затем я получаю:

root@c18df30d6d22:/usr/src/app# bundle exec rspec spec/models/article_spec.rb:174
database: test
Run options: include {:locations=>{"./spec/models/article_spec.rb"=>[174]}}
There was an error creating the elasticsearch index for Article: #<NameError: uninitialized constant Faraday::Error::ConnectionFailed>
There was an error removing the elasticsearch index for Article: #<NameError: uninitialized constant Faraday::Error::ConnectionFailed>

Я предполагаю, что набор тестов пытается инициализировать вещи, но веб-макет мешает...


person Erik Jacobs    schedule 19.06.2020    source источник


Ответы (1)


В итоге я отказался от Фарадея и от более сложного теста в качестве подхода. Я разложил worker на класс Service и worker. Рабочий просто вызывает класс Service. Это позволяет мне напрямую протестировать класс службы, а затем просто проверить, правильно ли рабочий процесс вызывает класс службы и что модель правильно вызывает рабочий процесс.

Вот гораздо более простой класс обслуживания:

require 'excon'

# this class is used to call out to the tokenizer service to retrieve
# a tokenized and vectorized JSON to store in an article model instance
class TokenizerVectorizerService
  def self.tokenize(content)
    tokenizer_url = ENV['TOKENIZER_URL']

    response = Excon.post("#{tokenizer_url}/tokenize",
               body: URI.encode_www_form(content: content),
               headers: { 'Content-Type' => 'application/x-www-form-urlencoded' },
               expects: [200])

    # the response's body contains the JSON for the tokenized and vectorized
    # article content
    response.body
  end
end

Вот тест, чтобы увидеть, что мы вызываем правильный пункт назначения:

require 'rails_helper'
require 'spec_helper'
require 'webmock/rspec'

RSpec.describe TokenizerVectorizerService, type: :service do

  describe "tokenize" do
    it "should send the content passed in" do
      request_url = "#{ENV['TOKENIZER_URL']}/tokenize"

      # webmock the expected request and response
      stub = stub_request(:post, request_url).
         with(
           body: {"content"=>"y"},
           headers: {
          'Content-Type'=>'application/x-www-form-urlencoded',
           }).
         to_return(status: 200, body: "y", headers: {})

      TokenizerVectorizerService.tokenize("y")
      expect(stub).to have_been_requested
    end
  end
end
person Erik Jacobs    schedule 20.06.2020