Как проверить, отправляет ли работник Sidekiq правильные данные во внешний API? - PullRequest
0 голосов
/ 19 июня 2020

У меня есть работник 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

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

Я предполагаю, что «правильный» способ сделать это - издеваться над ответами с помощью Фарадея, например что я ожидаю ответа c на конкретный c ввод. Создавая статью с содержанием 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 с определенным c телом ...

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

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

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

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

Я еще немного покопался и подумал: «Хорошо, позвольте мне проверить, что я отправив запрос, который я ожидаю, и что я правильно обрабатываю ответ. Итак, я попытался использовать 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

Но я думаю, что webmock не обрабатывается внутри задания 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 в любом другом месте, например, в начале моего файла, случайные вещи начнут взрываться. Например, если у меня есть эти строки в начале этого файла spe c:

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>

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

1 Ответ

0 голосов
/ 20 июня 2020

В итоге я отказался от Фарадея и более сложного теста как подхода. Я разделил воркера на класс обслуживания и воркера. Рабочий просто вызывает класс 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
...