【问题标题】:Mock out instance variable/accessor for a class instance using RSpec使用 RSpec 模拟类实例的实例变量/访问器
【发布时间】:2020-08-20 08:59:53
【问题描述】:

在我们的 Rails 应用程序中,我们有一个第三方 API(使用 Thrift),我们用类进行包装,这些类可以使用多种方法从同一实例中检索数据,然后将该数据添加到实例变量/访问器中。

例如,我们有一个像这样的BookManager 类:

class BookManager
  attr_accessor :token, :books, :scope, :total_count

  def initialize(token, scope, attrs={})
    @token = token
    @scope = scope
    @books = []
    @total_count = 0
  end

  # find all books
  def find_books
    @books = API.find_books(@token, @scope)
    @total_count = @books.count
  
    self
  end

  # find a single book by book_id
  def find_book_by_id(book_id)
    @books = API.find_book_by_id(@token, @scope, book_id)

    self
  end

  # find a single book by author_id
  def find_book_by_author_id(author_id)
    @books = API.find_book_by_author_id(@token, @scope, author_id)

    self
  end
end

所以在这里我们可以通过book_idauthor_id 获取书籍列表或一本书,然后API 将返回数据,我们的类实例将拥有这些书籍。

这样构建这个类的主要原因是因为 API 为每个数据实体设计了一个端点,我们必须使用多种方法来获取整个数据集,例如,如果我们想检索作者对于书籍,我们会使用如下方法:

def with_authors(&block)
  books.each do |book|
    book.author = API.find_author_by_id(@token, @scope, book.author_id, &block)
  end

  self
end

该类在我们的应用程序中使用如下:

book_manger = BookManager.new(current_user.token, params[:scope])
                         .find_book_by_id(params[:id])
@book = book_manger.books.first

或者如果我们也想要作者,我们会链接方法:

book_manger = BookManager.new(current_user.token, params[:scope])
                         .find_book_by_id(params[:id])
                         .with_authors
@book = book_manger.books.first

然后我们可以像这样访问数据:

@book.book_name
@book.author.author_name

希望到目前为止这一切都有意义......


因此,当我们为我们的应用编写 RSpec 测试时,我们希望模拟出这个 BookManager,这样它就不会调用实际的 API。

例如,在这里我创建了书籍的双份,并告诉 RSpec 在调用 find_book_by_id 方法时返回书籍(里面有书籍)。

book = double('book', book_id: 1, book_name: 'Book Name')
books = double('books', books: [book])
allow_any_instance_of(BookManager).to receive(:find_book_by_id).and_return(books)

但是我发现books 访问器总是返回它的默认值[],所以它实际上并没有使用我的模拟在类实例中设置@books

相反,我不得不模拟 API 本身:

book = double('book', book_id: 1, book_name: 'Book Name')
books = double('books', books: [book])
allow(API).to receive(:find_book_by_id).and_return(books)

然后允许我使用 BookManager... 这可以说是更好的实践,因为它是需要模拟的 API...但是我们的其他一些类有很多嵌套的 API 方法,我希望为了让模拟更简单,只模拟代码中使用的类,而不是下面的嵌套方法...... 我也很好奇我怎么能做到!

我假设 BookManager 的模拟没有按预期工作,因为我已经模拟了该方法(在这种情况下 find_book_by_id) which is what actual sets @booksand therefore the accessor/instance variable is always empty... so in this particular case, using.and_return(books)` 实际上并没有返回书……

似乎我需要做的是返回该类的实例,而不仅仅是 books,但我不确定如何使用 RSpec 模拟来做到这一点。

【问题讨论】:

  • 做以下工作:let(:stub_manager) { instance_double(BookManager, find_book_by_id: books) }allow(BookManager).to receive(:new).and_return(stub_manager)
  • 您可以根据规范的需要为方法定义一个fake implementation 吗?喜欢:allow(book_manager).to receive(:find_book_by_id) { |id| book_manager.instance_variable_set(:'@books', books); book_manager }?
  • 为努力编写一个清晰、结构良好的问题,并使用完美数量的代码来说明问题而受到支持。谢谢!

标签: ruby-on-rails ruby rspec


【解决方案1】:

您对为什么您尝试的存根不起作用是正确的。由于您正在模拟设置实例变量的方法,因此每当您通过 attr_accessor 访问实例变量时,您将获得初始化值而不是 find_books_by_id 的模拟返回值。

您不模拟 API 的直觉也是正确的。如果您的目标是测试使用BookManager 的代码,那么您应该模拟/存根BookManager 接口而不是其从属对象。事实上,您的测试不应该知道BookManager 的内部结构,包括它是否保持状态。那将违反得墨忒耳法则。

但是,您的测试确实知道BookManager 的公共接口,包括books attr_accessor。您的问题的解决方案是将其存根,并使用空对象模拟所有其他方法。

像这样:

let(:book_manager) { double(BookManager).as_null_object }
let(:book) { double('book', book_id: 1, book_name: 'Book Name') }
let(:books) { [book] }

before do
  allow(BookManager).to receive(:new).and_return(book_manager)
  allow(book_manager).to receive(:books).and_return(books)
end

现在,对find_book_by_idwith_authors 的调用将执行并返回与您的方法链完美配合的空对象(本质上是self)。而且,您可以只存根您关心的方法,例如 books

另外,如果您不使用 allow_any_instance_of,您将获得奖励积分,该积分应保留用于测试最棘手的遗留代码。

文档:https://relishapp.com/rspec/rspec-mocks/docs/basics/null-object-doubles

【讨论】:

  • 感谢您的帮助。我会试试的。有趣的是,您在我们使用该负载时避免使用allow_any_instance_of(这真的被认为是不好的做法吗?)。顺便说一句,如果我们确实想使用它...您将如何以与您所做的相同但使用 allow_any_instance_of 的方式模拟数据...只是对学习感到好奇。再次感谢。
  • 我尝试了上述方法,但 RSpec 给出了错误:RSpec::Mocks::MockExpectationError: the BookManager class does not implement the instance method: ancestors. Perhaps you meant to use 'class_double 而不是?``
  • 尝试使用“double”而不是“instance_double”。
  • 另外,这里是关于使用 allow_any_instance_of 的 RSpec 文档以及他们建议您避免使用它的原因:relishapp.com/rspec/rspec-mocks/docs/working-with-legacy-code/…
  • 至于我将如何使用allow_any_instance_of,我不会。对于最棘手的遗留代码问题,我保留使用该命令。即使那样,如果我能避免它,我也会这样做。
猜你喜欢
  • 2016-10-13
  • 1970-01-01
  • 1970-01-01
  • 2013-07-17
  • 1970-01-01
  • 2011-02-21
  • 1970-01-01
  • 2022-11-01
  • 1970-01-01
相关资源
最近更新 更多