【问题标题】:How do I test whether a Sidekiq worker is sending the right data to an external API?如何测试 Sidekiq 工作人员是否将正确的数据发送到外部 API?
【发布时间】:2020-06-19 01:12:27
【问题描述】:

我有一个 Sidekiq 工作人员,它通过外部 API 获取一些数据。我正在尝试编写测试以确保该工作人员的设计和功能正常。工作人员抓取一个本地模型实例并检查模型上的两个字段。如果其中一个字段是nil,它会将other字段发送到远程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,则文章摘要就是发送要编码的内容。

我的假设是,这样做的“正确”方法是使用法拉第模拟响应,以便我期望对特定输入的特定响应。通过创建包含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

但这用:post 模拟了法拉第 100% 的使用。在我的特殊情况下,我不知道如何用特定的身体来模拟 :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 正在干扰......

【问题讨论】:

    标签: ruby-on-rails unit-testing rspec faraday rspec-mocks


    【解决方案1】:

    我最终放弃了法拉第和更复杂的测试作为一种方法。我将工人分解为服务类和工人。工作人员只需调用 Service 类。这让我可以直接测试服务类,然后验证worker是否正确调用了服务类,以及模型是否正确调用了worker。

    这是更简单的服务类:

    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
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-11-05
      • 1970-01-01
      • 2019-01-16
      • 1970-01-01
      • 2013-09-16
      • 2015-03-10
      • 1970-01-01
      • 2014-05-02
      相关资源
      最近更新 更多