RSpec을 사용하여 클래스 인스턴스에 대한 인스턴스 변수 / 접근자를 모의합니다.
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_id
또는 author_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를 호출하지 않도록 이를 조롱하고 싶습니다 .
예를 들어 여기에서는 책의 이중을 만들고 find_book_by_id
메서드가 호출 될 때 책을 반환하도록 RSpec에 지시했습니다 .
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
내 mock을 사용하여 클래스 인스턴스 내부를 설정하지 않는다는 것 입니다.
대신 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
@books and therefore the accessor/instance variable is always empty... so in this particular case, using
.and_return (books)`는 실제로 책을 반환하지 않습니다 ...
내가해야 할 일은 해당 클래스의 인스턴스를 반환하는 것이 아니라 books
RSpec 모의를 사용하여 정확히 어떻게 할 것인지 잘 모르겠습니다.
답변
시도한 스텁이 작동하지 않는 이유에 대해 정확합니다. 인스턴스 변수를 설정하는 메서드를 모의하기 때문에를 통해 인스턴스 변수에 액세스 할 때마다 모의 attr_accessor
반환 값이 아닌 초기화 된 값을 얻게 find_books_by_id
됩니다.
API를 조롱하지 않는 본능도 정확합니다. 를 사용하는 코드를 테스트하는 것이 목표 인 경우 종속 개체가 아닌 인터페이스를 BookManager
모의 / 스텁해야 BookManager
합니다. 실제로 테스트는 BookManager
상태를 유지하는지 여부를 포함하여 의 내부 구조에 대해 알면 안됩니다 . 그것은 데메테르의 법칙을 위반하는 것입니다.
그러나 테스트는 attr_accessor를 BookManager
포함하여 의 공용 인터페이스에 대해 알고 books
있습니다. 문제에 대한 해결책은 그것을 스텁하고 다른 모든 메소드를 null 객체로 조롱하는 것입니다.
이렇게 :
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
이제 메서드 체인과 완벽하게 작동하는 null 개체 (본질적으로 self)를 호출 find_book_by_id
하고 with_authors
실행하고 반환합니다. 그리고 .NET과 같이 관심있는 메서드 만 스텁 할 수 있습니다 books
.
또한 allow_any_instance_of
가장 까다로운 레거시 코드를 테스트하기 위해 예약해야하는 사용하지 않으면 보너스 포인트를 받게 됩니다.
문서 : https://relishapp.com/rspec/rspec-mocks/docs/basics/null-object-doubles