0

我有一个 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特定身体的反应......

我也有可能要测试这一切都是错误的。相反,我可以测试我们是否发送了正确的内容(测试应该检查法拉第发送的内容)并完全忽略正确的响应。

测试该工作人员是否做正确事情的正确方法是什么(发送内容,或者如果内容为 nil 则发送摘要)?是测试发送的内容,还是测试我们返回的内容以反映发送的内容?

如果我应该测试返回的内容以反映正在发送的内容,我如何根据发送给它的东西的价值来模拟法拉第的不同响应/

** 稍后添加注释 **

我做了更多的挖掘和思考,好吧,让我测试一下我正在发送我期望的请求,并且我正在正确处理响应。所以,我尝试使用 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在其他任何地方包含,例如,在我的文件的开头,随机的东西就会开始爆炸。例如,如果我在这个规范文件的开头有这些行:

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 正在干扰......

4

1 回答 1

1

我最终放弃了法拉第和更复杂的测试作为一种方法。我将工人分解为服务类和工人。工作人员只需调用 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
于 2020-06-20T00:24:43.513 回答