ruby-on-rails 对于一个为GET请求返回不确定结果的控制器,我应该如何编写测试

2skhul33  于 2022-12-30  发布在  Ruby
关注(0)|答案(1)|浏览(80)

让我把我的问题放到一个我能想到的最简单的例子中,这个例子有点类似。
1.我有一个数据库表“Article”,它有一个名为“weight”的整数属性和一个名为“name”的字符串属性
1.我在数据库中有10篇不同权重和名称的文章
1.我收到一个包含“name”作为参数的GET请求
1.在这10篇文章中,我找到了同名的文章。我找到了4篇文章。
1.在这4篇文章中,我选择一个随机选择的,但是随机性是加权的,权重越大,被选中的机会越大。因此,如果四个权重是1、3、5、7,我生成一个0和权重之和之间的随机数(即1+3+5+7=16),然后用这个数字来决定选择哪篇文章,所以如果我得到的随机数是0,那么我选择第一篇文章,如果我得到的随机数是1~3,我选择第二条,如果我得到的随机数是4~8,我选择第三条,如果我得到的随机数是9~15,我选择第四条。
1.我发送这篇文章作为GET请求的响应。
正如你所看到的,对于一个GET请求的响应,如果有一个特定的“name”作为参数,那么响应并不总是相同的。然而,我找到的所有关于测试Controller的例子都依赖于请求的响应是确定的。
所以我想得到一些帮助,关于我将如何去设计我的测试代码。
我目前对测试的想法如下:
1.就GET请求的响应而言,如果GET查询不包括name参数,那么我预期HTTP状态代码为400,并显示消息“Bad parameters”
1.如果GET包含name参数,那么我期望HTTP Status代码为200(但是不要对内容做任何额外的检查)
1.如果GET包含一个与任何Articles都不匹配的名称,那么我期望HTTP Status代码为200,响应消息为“null”。
1.我用一个名字做了一个GET请求,在我做随机选择之前,我得到了与“名字”匹配的文章的正确列表。
1.我创建了4个具有相同“名称”的Article对象,每个对象的权重为1、2、3、4我在进行加权随机选择的逻辑上运行10,000次。我检查我获得第一篇文章的次数是否在700到1300之间(约1/10,000),如果我得到第二篇文章的次数在1700到2300之间(约万分之二),如果我得到第三篇文章的次数在2700到3300之间(约万分之三),如果我拿到第四篇文章的次数在3700到4300之间(约万分之四)。
一个问题是,尽管试图阅读如何在RubyonRails上编写测试,我仍然不确定如何做4和5。4和5要求我在逻辑“内部”测试事物(而不是使用执行整个逻辑的结果)。看起来我需要把逻辑分解成更小的函数,可以调用来测试4和5。对吗?我还应该测试哪些东西?
下面是我创建的一个简单代码,以便我们可以按照这个示例进行沿着。完整代码如下:https://github.com/maruhanpark/example
你可以用docker compose运行它。
我上面解释的测试代码如下所示:

require 'test_helper'

class ArticleControllerTest < ActionDispatch::IntegrationTest
  test "having no name parameter returns Bad Parameters" do
    get "/article"
    assert_response(400)
    assert_equal "{\"message\":\"Bad parameters\"}", @response.body
  end

  test "having name parameter returns 200" do
    get "/article?name=Bob"
    assert_response(200)
  end

  test "having empty name parameter returns 200 and null" do
    get "/article?name="
    assert_response(200)
    assert_equal "null", @response.body
  end

  test "getSelectedArticles returns the correct list of articles" do
  end

  test "randomWeight selects properly based on weight" do
  end
end

正如你所看到的,我写了我解释的前三个案例,但是遗漏了案例4和5。
对于控制器,我把我想测试的逻辑分解为单独的方法,我创建的两个方法分别用于测试用例4和5,我不确定是否有必要创建单独的方法来测试那个逻辑。

class ArticleController < ApplicationController
    def getSelectedArticles(name:)
        #logger.debug "===== #{Article.where(name: name)}"
        return Article.where(name: name)
    end

    #Returns the index of the finally selected article
    def randomWeight(articles:)
        sum = 0
        articles.each { |a| sum += a.weight }
        randNum = rand(sum)
        returnIndex = 0
        for i in 0 ... articles.size
            randNum -= articles[i].weight
            if (randNum < 0)
                returnIndex = i
                break
            end
        end
        return returnIndex
    end

    def article
        if params[:name]
            selectedArticles = getSelectedArticles(name: params[:name])
            finalArticle = selectedArticles[randomWeight(articles: selectedArticles)]
            render json: finalArticle, status: :ok
        else
            render json: {message: "Bad parameters"}, status: :bad_request
        end
    end

end

我在尝试实现测试4时遇到的一个问题是,我需要在调用GET之后,以某种方式让代码在getSelectedArticles方法上有一个断点。我不确定这是否可能,或者我是否必须只作为一种单元测试来测试它,在这种测试中,我专门调用该方法,并看到该方法返回预期的东西。

zpgglvta

zpgglvta1#

我阅读了您的请求,并注意到我们可以对您的代码进行以下改进:
1.请使用REST路由,即在routes.rb中请写入resources :articles, only: [:show],将控制器命名为ArticlesController(注意复数),您的路由将为/articles/:id
1.方法和变量名请使用两个空格缩进和snake_case
1.鼓励使用private来分隔方法
1.在方法的最后语句中不需要“return
1.在ruby中选择随机的文章可能不是很好,如果你有很多文章,我建议在数据库上这样做:SQL技能方面的工作量会稍大一些,但速度会更快,扩展性也会更好
1.我建议您将控制器的内容与服务分离,这样更易于测试和开发

class SearchArticles
  def call(params)
    # All your logic here
  end
end

class ArticlesController < ApplicationController
  def article
    render json: { message: 'Bad parameters' }, status: :bad_request unless params[:name].present?

    final_article = SearchArticles.new.call(params)
    render json: final_article, status: :ok
  end

无论如何,这里是我的第一次通过的改进:

class ArticlesController < ApplicationController
  def article
    render json: { message: 'Bad parameters' }, status: :bad_request unless params[:name].present?

    selected_articles = search_articles(name: params[:name])
    final_article = selected_articles[random_weight(articles: selected_articles)]
    render json: final_article, status: :ok
  end

  private

  def search_articles(name:)
    Article.where('name LIKE ?', name)
  end

  def random_weight(articles:)
    sum = articles.pluck(:weight).sum
    rand_num = rand(sum)
    return_index = 0
    # This could be improved!
    for i in 0...articles.size
      rand_num -= articles[i].weight
      if (rand_num < 0)
        return_index = i
        break
      end
    end
    return_index
  end
end

测试

由于我对minitest了解不多,我将为RSpec写一个答案,希望您能够自己转换它(警告,未经测试!):

describe ArticlesController do
  describe '#article' do
    before do
      create(:article, id: 11, name: 'Bob 11', weight: 1)
      create(:article, id: 22, name: 'Bob 22', weight: 3)
      create(:article, id: 33, name: 'Bob 33', weight: 5)
      create(:article, id: 44, name: 'Bob 44', weight: 7)
    end

    it 'returns the correct article' do
      {
        0 => 11,
        1 => 22,
        3 => 22,
        4 => 33,
        8 => 33,
        9 => 44,
        15 => 44,
      }.each do |random_number, expected_article_id|
        allow_any_instance_of(Object)
          .to receive(:rand).and_return(random_number)

        get "/article?name=Bob"

        result = JSON.parse(response.body).id # please continue from here
        expect(result).to eq(expected_article_id)
      end
    end
  end
end

请注意,以上都不是很好的状态,它应该被视为一个很好的起点。
祝你好运伙计!

相关问题