Instanzvariable/Accessor für eine Klasseninstanz mit RSpec nachahmen
In unserer Rails-Anwendung haben wir eine Drittanbieter-API (unter Verwendung von Thrift), die wir mit Klassen umschließen, die mehrere Methoden zum Abrufen von Daten aus derselben Instanz verwenden und diese Daten dann zu einer Instanzvariablen/einem Zugriffselement hinzufügen können.
Zum Beispiel haben wir eine BookManagerKlasse wie folgt:
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
Hier können wir also eine Liste von Büchern oder ein einzelnes Buch mit book_idoder author_idabrufen, und dann gibt die API die Daten zurück, und unsere Klasseninstanz enthält diese Bücher.
Der Hauptgrund, warum diese Klasse so aufgebaut ist, liegt darin, dass die API mit einem Endpunkt für jede Datenentität entworfen wurde und wir mehrere Methoden verwenden müssen, um den gesamten Datensatz zu erhalten, also zum Beispiel, wenn wir die Autoren für die Bücher abrufen wollten Wir würden eine Methode verwenden wie:
def with_authors(&block)
books.each do |book|
book.author = API.find_author_by_id(@token, @scope, book.author_id, &block)
end
self
end
Die Klasse wird in unserer Anwendung wie folgt verwendet:
book_manger = BookManager.new(current_user.token, params[:scope])
.find_book_by_id(params[:id])
@book = book_manger.books.first
Oder wenn wir auch die Autoren wollten, würden wir die Methoden verketten:
book_manger = BookManager.new(current_user.token, params[:scope])
.find_book_by_id(params[:id])
.with_authors
@book = book_manger.books.first
Und dann können wir auf die Daten zugreifen wie:
@book.book_name
@book.author.author_name
Hoffentlich macht das alles bis jetzt Sinn...
Wenn wir also RSpec-Tests für unsere App schreiben, möchten wir dies verspotten, BookManagerdamit es nicht die eigentliche API aufruft.
Zum Beispiel habe ich hier Doubles der Bücher erstellt und RSpec angewiesen, die Bücher (mit dem Buch darin) zurückzugeben, wenn die find_book_by_idMethode aufgerufen wird.
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)
Was ich jedoch festgestellt habe, ist, dass der booksAccessor immer seinen Standardwert von zurückgibt [], sodass er nicht wirklich die @booksinnerhalb der Klasseninstanz mit meinem Mock festlegt.
Stattdessen musste ich die API selbst verspotten:
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)
Was mir dann erlaubt, das BookManager... zu verwenden, was als bessere Praxis argumentiert werden könnte, da es die API ist, die verspottet werden muss ... aber einige unserer anderen Klassen haben viele verschachtelte API-Methoden, und ich hatte gehofft, das Verspotten einfacher und einfacher zu halten verspotten Sie nur die im Code verwendeten Klassen und nicht die verschachtelten Methoden unten ... Ich bin auch neugierig, wie ich das machen könnte!
Ich gehe davon aus, dass das Verspotten von BookManagernicht wie erwartet funktioniert, weil ich die Methode verspottet habe (in diesem Fall find_book_by_id) which is what actual sets gibt @books and therefore the accessor/instance variable is always empty... so in this particular case, using.and_return(books)` die Bücher nicht wirklich zurück ...
Anscheinend muss ich die Instanz dieser Klasse zurückgeben und nicht nur die books, aber ich bin mir nicht sicher, wie ich das mit RSpec-Mocks machen würde.
Antworten
Sie haben Recht, warum der Stub, den Sie versucht haben, nicht funktioniert. Da Sie die Methode verspotten, die die Instanzvariable festlegt, erhalten Sie jedes Mal, wenn Sie über die auf die Instanzvariable zugreifen attr_accessor, den initialisierten Wert und nicht den verspotteten Rückgabewert von find_books_by_id.
Sie haben auch recht in Ihrem Instinkt, die API nicht zu verspotten. Wenn Sie den Code testen möchten, der die verwendet , sollten Sie die Schnittstelle und nicht ihre untergeordneten Objekte BookManagermocken/stubn . BookManagerTatsächlich sollten Ihre Tests nichts über die interne Struktur der wissen BookManager, einschließlich dessen, ob sie den Zustand beibehält oder nicht. Das wäre ein Verstoß gegen das Demeter-Gesetz.
Ihre Tests kennen jedoch die öffentliche Schnittstelle von BookManager, einschließlich des booksattr_accessor. Die Lösung für Ihr Problem besteht darin, das zu stumpfen und alle anderen Methoden mit einem Nullobjekt zu verspotten.
So was:
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
Nun werden Aufrufe an find_book_by_idund with_authorsausgeführt und das Nullobjekt (im Wesentlichen selbst) zurückgegeben, das perfekt mit Ihrer Methodenverkettung funktioniert. Und Sie können nur die Methoden stubn, die Ihnen wichtig sind, wie books.
Außerdem erhalten Sie Bonuspunkte für allow_any_instance_ofdie Nichtverwendung, die für das Testen des heikelsten Legacy-Codes reserviert werden sollten.
Dokumente:https://relishapp.com/rspec/rspec-mocks/docs/basics/null-object-doubles