Mengolok-olok variabel / pengakses instance untuk instance kelas menggunakan RSpec
Dalam aplikasi Rails kami memiliki API pihak ketiga (menggunakan Hemat) yang kami bungkus dengan kelas yang dapat menggunakan beberapa metode untuk mengambil data dari contoh yang sama dan kemudian menambahkan data itu ke dalam variabel / pengakses contoh.
Misalnya kami memiliki BookManager
kelas seperti ini:
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
Jadi di sini kita bisa mendapatkan daftar buku, atau satu buku dengan book_id
atau author_id
dan kemudian API akan mengembalikan data dan instance kelas kita akan memiliki buku-buku ini.
Alasan utama kelas ini dibangun seperti ini adalah karena API dirancang dengan titik akhir untuk setiap entitas data dan kita harus menggunakan beberapa metode untuk mendapatkan seluruh kumpulan data, jadi misalnya jika kita ingin mengambil penulis untuk bukunya kami akan menggunakan metode seperti:
def with_authors(&block)
books.each do |book|
book.author = API.find_author_by_id(@token, @scope, book.author_id, &block)
end
self
end
Kelas digunakan dalam aplikasi kita seperti ini:
book_manger = BookManager.new(current_user.token, params[:scope])
.find_book_by_id(params[:id])
@book = book_manger.books.first
Atau jika kami menginginkan penulisnya juga, kami akan merangkai metode:
book_manger = BookManager.new(current_user.token, params[:scope])
.find_book_by_id(params[:id])
.with_authors
@book = book_manger.books.first
Dan kemudian kita dapat mengakses data seperti:
@book.book_name
@book.author.author_name
Semoga ini semua masuk akal sampai sekarang ...
Jadi saat kami menulis pengujian RSpec untuk aplikasi kami, kami ingin meniru ini BookManager
sehingga tidak memanggil API yang sebenarnya.
Misalnya di sini saya telah membuat buku ganda dan memberi tahu RSpec untuk mengembalikan buku (dengan buku di dalamnya) ketika find_book_by_id
metode ini dipanggil.
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)
Namun apa yang saya temukan adalah bahwa books
pengakses selalu mengembalikan nilai defaultnya []
, jadi itu tidak benar-benar mengatur @books
instance di dalam kelas menggunakan tiruan saya.
Sebaliknya, saya harus mengejek API itu sendiri:
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)
Yang kemudian memungkinkan saya untuk menggunakan BookManager
... yang bisa dikatakan sebagai praktik yang lebih baik karena API-lah yang perlu diejek ... tetapi beberapa kelas kami yang lain memiliki banyak metode API bersarang dan saya berharap untuk menjaga agar ejekan lebih sederhana dan hanya mengejek kelas yang digunakan dalam kode daripada metode bersarang di bawah ini ... Saya juga ingin tahu bagaimana saya bisa melakukannya!
Saya berasumsi bahwa mengejek yang BookManager
tidak berfungsi seperti yang diharapkan karena saya telah mengejek metode (dalam hal ini 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) `tidak benar-benar mengembalikan buku ...
Sepertinya yang perlu saya lakukan adalah mengembalikan instance kelas itu daripada hanya books
tetapi saya tidak yakin bagaimana saya akan melakukannya menggunakan tiruan RSpec.
Jawaban
Anda benar tentang mengapa rintisan yang Anda coba tidak berhasil. Karena Anda mengejek metode yang menyetel variabel contoh, setiap kali Anda mengakses variabel contoh melalui, attr_accessor
Anda akan mendapatkan nilai yang diinisialisasi daripada nilai kembalian palsu find_books_by_id
.
Naluri Anda juga benar untuk tidak mengejek API. Jika tujuan Anda adalah untuk menguji kode yang menggunakan BookManager
, maka Anda harus membuat tiruan / stub BookManager
antarmuka bukan objek bawahannya. Faktanya, pengujian Anda seharusnya tidak mengetahui apa pun tentang struktur internal BookManager
, termasuk apakah ia mempertahankan status atau tidak. Itu akan menjadi pelanggaran Hukum Demeter.
Namun, pengujian Anda mengetahui tentang antarmuka publik dari BookManager
, termasuk books
attr_accessor. Solusi untuk masalah Anda adalah dengan menghentikan itu, dan mengejek semua metode lain dengan objek null.
Seperti ini:
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
Sekarang, panggilan ke find_book_by_id
dan with_authors
akan mengeksekusi dan mengembalikan objek null (self, pada dasarnya) yang berfungsi sempurna dengan rangkaian metode Anda. Dan, Anda dapat menghentikan hanya metode yang Anda pedulikan, seperti books
.
Plus, Anda akan mendapatkan poin bonus karena tidak digunakan allow_any_instance_of
yang seharusnya disediakan untuk menguji kode warisan yang paling sulit.
Dokumen: https://relishapp.com/rspec/rspec-mocks/docs/basics/null-object-doubles