Khi nào nên sử dụng RSpec let ()?


447

Tôi có xu hướng sử dụng trước các khối để đặt các biến thể hiện. Sau đó tôi sử dụng các biến đó qua các ví dụ của tôi. Gần đây tôi đã đến let(). Theo tài liệu RSpec, nó được sử dụng để

... Để xác định một phương thức trợ giúp ghi nhớ. Giá trị sẽ được lưu trong nhiều cuộc gọi trong cùng một ví dụ nhưng không phải qua các ví dụ.

Điều này khác với việc sử dụng các biến thể hiện trong các khối trước như thế nào? Và khi nào bạn nên sử dụng let()vs before()?


1
Hãy để các khối được đánh giá một cách lười biếng, trong khi trước khi các khối chạy trước mỗi ví dụ (chúng chậm hơn). Sử dụng trước các khối phụ thuộc vào sở thích cá nhân (kiểu mã hóa, giả / sơ khai ...). Hãy để khối thường được ưa thích. Bạn có thể kiểm tra thông tin
Nesha Zoric

Nó không phải là thực hành tốt để đặt các biến thể hiện trong một hook trước. Kiểm tra betterspecs.org
Allison

Câu trả lời:


604

Tôi luôn thích letmột biến đối tượng vì một vài lý do:

  • Biến sơ thẩm mùa xuân tồn tại khi được tham chiếu. Điều này có nghĩa là nếu bạn mập ngón tay đánh vần biến cá thể, một cái mới sẽ được tạo và khởi tạo nil, điều này có thể dẫn đến các lỗi tinh vi và dương tính giả. Kể từ khi lettạo một phương thức, bạn sẽ nhận được NameErrorkhi bạn viết sai chính tả, điều mà tôi thấy thích hợp hơn. Nó làm cho nó dễ dàng hơn để tái cấu trúc thông số kỹ thuật, quá.
  • Một before(:each)hook sẽ chạy trước mỗi ví dụ, ngay cả khi ví dụ này không sử dụng bất kỳ biến đối tượng nào được xác định trong hook. Đây thường không phải là một vấn đề lớn, nhưng nếu việc thiết lập biến thể hiện mất nhiều thời gian, thì bạn đang lãng phí chu kỳ. Đối với phương thức được xác định bởi let, mã khởi tạo chỉ chạy nếu ví dụ gọi nó.
  • Bạn có thể cấu trúc lại từ một biến cục bộ trong một ví dụ trực tiếp thành cho phép mà không thay đổi cú pháp tham chiếu trong ví dụ. Nếu bạn tái cấu trúc thành một biến thể hiện, bạn phải thay đổi cách bạn tham chiếu đối tượng trong ví dụ (ví dụ: thêm một @).
  • Đây là một chút chủ quan, nhưng như Mike Lewis đã chỉ ra, tôi nghĩ rằng nó làm cho thông số kỹ thuật dễ đọc hơn. Tôi thích tổ chức xác định tất cả các đối tượng phụ thuộc của tôi với letvà giữ cho itkhối của tôi đẹp và ngắn.

Một liên kết liên quan có thể được tìm thấy ở đây: http://www.betterspecs.org/#let


2
Tôi thực sự thích lợi thế đầu tiên bạn đề cập, nhưng bạn có thể giải thích thêm một chút về cái thứ ba không? Cho đến nay các ví dụ tôi đã thấy (thông số kỹ thuật mongoid : github.com/mongoid/mongoid/blob/master/spec/feftal/mongoid/iêu ) sử dụng các khối dòng đơn và tôi không thấy cách không có "@" dễ đọc hơn
đã gửi đến

6
Như tôi đã nói, nó hơi chủ quan, nhưng tôi thấy nó hữu ích khi sử dụng letđể xác định tất cả các đối tượng phụ thuộc và sử dụng before(:each)để thiết lập cấu hình cần thiết hoặc bất kỳ giả định / sơ khai nào cần thiết trong các ví dụ. Tôi thích cái này hơn một cái trước khi hook chứa tất cả cái này. Ngoài ra, let(:foo) { Foo.new }là ít ồn ào hơn (và nhiều hơn đến điểm) sau đó before(:each) { @foo = Foo.new }. Đây là một ví dụ về cách tôi sử dụng nó: github.com/myronmarston/vcr/blob/v1.7.0/spec/vcr/util/ Kẻ
Myron Marston

Cảm ơn ví dụ, điều đó thực sự có ích.
đã gửi đến

3
Andrew Grimm: đúng, nhưng các cảnh báo có thể tạo ra vô số tiếng ồn (tức là từ đá quý mà bạn sử dụng không chạy cảnh báo). Thêm vào đó, tôi thích nhận NoMethodErrorđược một cảnh báo, nhưng YMMV.
Myron Marston

1
@ Jwan622: bạn có thể bắt đầu bằng cách viết một ví dụ, trong đó có foo = Foo.new(...)và sau đó người dùng fooở các dòng sau. Sau đó, bạn viết một ví dụ mới trong cùng một nhóm ví dụ cũng cần được Fookhởi tạo theo cùng một cách. Tại thời điểm này, bạn muốn cấu trúc lại để loại bỏ sự trùng lặp. Bạn có thể xóa các foo = Foo.new(...)dòng khỏi ví dụ của mình và thay thế nó bằng let(:foo) { Foo.new(...) }cách thay đổi cách sử dụng các ví dụ foo. Nhưng nếu bạn tái cấu trúc cho before { @foo = Foo.new(...) }bạn cũng phải cập nhật các tài liệu tham khảo trong các ví dụ từ foođến @foo.
Myron Marston

82

Sự khác biệt giữa việc sử dụng các biến thể hiện và let()đó let()đánh giá lười biếng . Điều này có nghĩa let()là không được đánh giá cho đến khi phương thức mà nó xác định được chạy lần đầu tiên.

Sự khác biệt giữa beforeletlet()cung cấp cho bạn một cách hay để xác định một nhóm biến theo kiểu 'xếp tầng'. Bằng cách này, thông số kỹ thuật có vẻ tốt hơn một chút bằng cách đơn giản hóa mã.


1
Tôi thấy, đó thực sự là một lợi thế? Mã đang được chạy cho mỗi ví dụ bất kể.
đã gửi đến

2
IMO dễ đọc hơn và khả năng đọc là một yếu tố rất lớn trong ngôn ngữ lập trình.
Mike Lewis

4
Senthil - nó thực sự không nhất thiết phải chạy trong mọi ví dụ khi bạn sử dụng let (). Nó lười biếng, vì vậy nó chỉ chạy nếu được tham chiếu. Nói chung, điều này không quan trọng lắm vì quan điểm của một nhóm ví dụ là có một vài ví dụ chạy trong một bối cảnh chung.
David Chelimsky

1
Vì vậy, điều đó có nghĩa là bạn không nên sử dụng letnếu bạn cần đánh giá thứ gì đó mỗi lần? ví dụ: tôi cần một mô hình con có mặt trong cơ sở dữ liệu trước khi một số hành vi được kích hoạt trên mô hình cha. Tôi không nhất thiết phải tham khảo mô hình con đó trong thử nghiệm, bởi vì tôi đang thử nghiệm hành vi của mô hình cha mẹ. Hiện tại tôi đang sử dụng let!phương thức này, nhưng có lẽ sẽ rõ ràng hơn khi đặt thiết lập đó vào before(:each)?
gar

2
@gar - Tôi sẽ sử dụng Factory (như FactoryGirl) cho phép bạn tạo các hiệp hội con cần thiết khi bạn khởi tạo cha mẹ. Nếu bạn làm theo cách này, thì nó không thực sự quan trọng nếu bạn sử dụng let () hoặc khối thiết lập. Let () là tốt nếu bạn không cần sử dụng MỌI THỨ cho mỗi bài kiểm tra trong ngữ cảnh phụ của bạn. Thiết lập chỉ nên có những gì cần thiết cho mỗi người.
Harmon

17

Tôi đã thay thế hoàn toàn tất cả việc sử dụng các biến thể hiện trong các thử nghiệm rspec của mình để sử dụng let (). Tôi đã viết một ví dụ nhanh cho một người bạn đã sử dụng nó để dạy một lớp Rspec nhỏ: http://ruby-lambda.blogspot.com/2011/02/agile-rspec-with-let.html

Như một số câu trả lời khác ở đây nói, let () lười đánh giá nên nó sẽ chỉ tải những câu hỏi yêu cầu tải. Nó DRYs lên thông số kỹ thuật và làm cho nó dễ đọc hơn. Trên thực tế, tôi đã chuyển mã Rspec let () để sử dụng trong các bộ điều khiển của mình, theo kiểu đá quý được thừa kế_resource. http://ruby-lambda.blogspot.com/2010/06/steals-let-from-rspec.html

Cùng với việc đánh giá lười biếng, ưu điểm khác là, kết hợp với ActiveSupport :: Mối quan tâm và thông số / hỗ trợ / hành vi tải mọi thứ trong ứng dụng, bạn có thể tạo đặc tả mini-DSL riêng cho ứng dụng của mình. Tôi đã viết những bài kiểm tra đối với tài nguyên Rack và RESTful.

Chiến lược tôi sử dụng là Factory-mọi thứ (thông qua Machinist + Forgery / Faker). Tuy nhiên, có thể sử dụng nó kết hợp với các khối trước (: mỗi) để tải trước các nhà máy cho toàn bộ các nhóm ví dụ, cho phép thông số kỹ thuật chạy nhanh hơn: http://makandra.com/notes/770-taking- nhược điểm -of-rspec-s-let-in-before-blocks


Này Ho-Sheng, tôi thực sự đã đọc một số bài đăng trên blog của bạn trước khi đặt câu hỏi này. Về bạn # spec/friendship_spec.rb# spec/comment_spec.rbdụ, đừng có nghĩ là họ làm cho nó ít có thể đọc được? Tôi không biết từ đâu usersđến và sẽ cần phải đào sâu hơn.
đã gửi đến

Hơn chục người đầu tiên tôi đã hiển thị định dạng cho tất cả mọi người thấy nó dễ đọc hơn nhiều, và một vài người trong số họ bắt đầu viết với nó. Bây giờ tôi đã có đủ mã spec bằng cách sử dụng let () mà tôi cũng gặp phải một số vấn đề đó. Tôi thấy mình đi vào ví dụ, và bắt đầu từ nhóm ví dụ trong cùng, tự mình làm việc sao lưu. Đó là kỹ năng tương tự như sử dụng môi trường lập trình siêu cao.
Ho-Sheng Hsiao

2
Gotcha lớn nhất mà tôi gặp phải là vô tình sử dụng let (: topic) {} thay vì chủ đề {}. chủ đề () được thiết lập khác với let (: chủ đề), nhưng let (: chủ đề) sẽ ghi đè lên nó.
Ho-Sheng Hsiao

1
Nếu bạn có thể "đi sâu" vào mã, thì bạn sẽ thấy việc quét mã với khai báo let () nhanh hơn nhiều. Dễ dàng chọn ra các khai báo let () khi quét mã hơn là tìm các biến @ được nhúng vào mã. Sử dụng @variabled, tôi không có "hình dạng" tốt cho dòng nào đề cập đến việc gán cho các biến và dòng nào đề cập đến việc kiểm tra các biến. Sử dụng let (), tất cả các bài tập được thực hiện với let () để bạn biết "ngay lập tức" bằng hình dạng của các chữ cái nơi khai báo của bạn.
Ho-Sheng Hsiao

1
Bạn có thể làm cho cùng một đối số về các biến đối tượng trở nên dễ dàng hơn để chọn ra, đặc biệt là vì một số trình soạn thảo, như các biến đối tượng của tôi (gedit) làm nổi bật các biến đối tượng. Tôi đã sử dụng let()vài ngày qua và cá nhân tôi không thấy sự khác biệt, ngoại trừ lợi thế đầu tiên Myron đã đề cập. Và tôi không chắc chắn về việc cho đi và những gì không, có lẽ vì tôi lười biếng và tôi thích xem mã trả trước mà không phải mở thêm một tập tin nào khác. Cảm ơn ý kiến ​​của bạn.
đã gửi-hil

13

Điều quan trọng cần lưu ý là hãy lười biếng đánh giá và không đưa các phương pháp tác dụng phụ vào đó nếu không bạn sẽ không thể thay đổi từ cho phép sang trước (: từng) một cách dễ dàng. Bạn có thể sử dụng cho phép! thay vì để nó được đánh giá trước mỗi kịch bản.


8

Nói chung, let()là một cú pháp đẹp hơn và nó giúp bạn tiết kiệm việc gõ @namecác ký hiệu ở mọi nơi. Nhưng, hãy cẩn thận! Tôi cũng đã tìm thấy let()các lỗi tinh vi (hoặc ít nhất là gãi đầu) bởi vì biến không thực sự tồn tại cho đến khi bạn cố gắng sử dụng nó ... Hãy kể dấu hiệu câu chuyện: nếu thêm một dấu putssau let()để thấy rằng biến đó chính xác cho phép một thông số kỹ thuật để vượt qua, nhưng không có putsthông số kỹ thuật thất bại - bạn đã tìm thấy sự tinh tế này.

Tôi cũng đã thấy rằng let()dường như không lưu trữ trong mọi trường hợp! Tôi đã viết nó lên blog của mình: http://technicaldebt.com/?p=1242

Có lẽ nó chỉ cho tôi được?


9
letluôn ghi nhớ giá trị trong suốt thời gian của một ví dụ. Nó không ghi nhớ giá trị qua nhiều ví dụ. before(:all), ngược lại, cho phép bạn sử dụng lại một biến khởi tạo trong nhiều ví dụ.
Myron Marston

2
nếu bạn muốn sử dụng let (như bây giờ dường như được coi là thực tiễn tốt nhất), nhưng cần một biến cụ thể để được khởi tạo ngay lập tức, đó là những gì let!được thiết kế cho. relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/ khăn
Jacob

6

hãy để nó hoạt động như một bản chất của nó. Cũng lưu trữ nó.

Một gotcha tôi tìm thấy ngay lập tức với let ... Trong một khối Spec đang đánh giá một sự thay đổi.

let(:object) {FactoryGirl.create :object}

expect {
  post :destroy, id: review.id
}.to change(Object, :count).by(-1)

Bạn sẽ cần chắc chắn gọi letbên ngoài khối mong đợi của bạn. tức là bạn đang gọi FactoryGirl.createtrong khối cho phép của bạn. Tôi thường làm điều này bằng cách xác minh đối tượng được duy trì.

object.persisted?.should eq true

Mặt khác, khi letkhối được gọi lần đầu tiên, một sự thay đổi trong cơ sở dữ liệu sẽ thực sự xảy ra do việc khởi tạo lười biếng.

Cập nhật

Chỉ cần thêm một ghi chú. Hãy cẩn thận khi chơi golf code hoặc trong trường hợp này rspec golf với câu trả lời này.

Trong trường hợp này, tôi chỉ cần gọi một số phương thức mà đối tượng đáp ứng. Vì vậy, tôi gọi _.persisted?phương thức _ trên đối tượng là sự thật của nó. Tất cả những gì tôi đang cố gắng làm là khởi tạo đối tượng. Bạn có thể gọi trống không? hay không quá. Vấn đề không phải là thử nghiệm mà là mang lại sự sống cho đối tượng bằng cách gọi nó.

Vì vậy, bạn không thể tái cấu trúc

object.persisted?.should eq true

được

object.should be_persisted 

vì đối tượng chưa được khởi tạo ... sự lười biếng của nó. :)

Cập nhật 2

tận dụng sự cho phép! cú pháp để tạo đối tượng ngay lập tức, nên tránh vấn đề này hoàn toàn. Lưu ý mặc dù nó sẽ đánh bại rất nhiều mục đích của sự lười biếng của không đập.

Ngoài ra, trong một số trường hợp, bạn thực sự có thể muốn tận dụng cú pháp chủ đề thay vì cho phép vì nó có thể cung cấp cho bạn các tùy chọn bổ sung.

subject(:object) {FactoryGirl.create :object}

2

Lưu ý với Joseph - nếu bạn đang tạo các đối tượng cơ sở dữ liệu, before(:all)chúng sẽ không bị bắt trong giao dịch và bạn có nhiều khả năng rời khỏi hành trình trong cơ sở dữ liệu thử nghiệm của mình. Sử dụng before(:each)thay thế.

Lý do khác để sử dụng let và đánh giá lười biếng của nó là vì vậy bạn có thể lấy một đối tượng phức tạp và kiểm tra từng phần riêng lẻ bằng cách ghi đè cho phép trong bối cảnh, như trong ví dụ rất giả định này:

context "foo" do
  let(:params) do
     { :foo => foo,  :bar => "bar" }
  end
  let(:foo) { "foo" }
  it "is set to foo" do
    params[:foo].should eq("foo")
  end
  context "when foo is bar" do
    let(:foo) { "bar" }
    # NOTE we didn't have to redefine params entirely!
    it "is set to bar" do
      params[:foo].should eq("bar")
    end
  end
end

1
+1 trước (: tất cả) lỗi đã lãng phí nhiều ngày thời gian của các nhà phát triển của chúng tôi.
Michael Durrant

1

"trước" theo mặc định ngụ ý before(:each). Tham khảo Sách Rspec, bản quyền 2010, trang 228.

before(scope = :each, options={}, &block)

Tôi sử dụng before(:each)để tạo một số dữ liệu cho từng nhóm ví dụ mà không phải gọi letphương thức để tạo dữ liệu trong khối "nó". Ít mã hơn trong khối "nó" trong trường hợp này.

Tôi sử dụng letnếu tôi muốn một số dữ liệu trong một số ví dụ nhưng không phải là dữ liệu khác.

Cả trước và sau đều tuyệt vời để DRYing lên các khối "nó".

Để tránh mọi sự nhầm lẫn, "cho phép" không giống như before(:all). "Hãy" đánh giá lại phương thức và giá trị của nó cho từng ví dụ ("nó"), nhưng lưu trữ giá trị qua nhiều cuộc gọi trong cùng một ví dụ. Bạn có thể đọc thêm về nó ở đây: https://www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let


1

Tiếng nói bất đồng ở đây: sau 5 năm rspec tôi không thích letlắm.

1. Đánh giá lười biếng thường làm cho thiết lập thử nghiệm khó hiểu

Lý do về việc thiết lập trở nên khó khăn khi một số thứ đã được khai báo trong thiết lập không thực sự ảnh hưởng đến trạng thái, trong khi những thứ khác thì không.

Cuối cùng, trong sự thất vọng một ai đó chỉ cần thay đổi letđể let!(điều tương tự mà không đánh giá lười biếng) để có được làm việc đặc điểm của chúng. Nếu điều này phù hợp với họ, một thói quen mới được sinh ra: khi một thông số mới được thêm vào một bộ cũ hơn và nó không hoạt động, điều đầu tiên người viết cố gắng là thêm bangs vào ngẫu nhiênlet cuộc gọi .

Khá sớm tất cả các lợi ích hiệu suất đã biến mất.

2. Cú pháp đặc biệt khác thường đối với người dùng không phải là rspec

Tôi thà dạy Ruby cho đội của mình hơn là những mánh khóe của rspec. Các biến sơ thẩm hoặc các cuộc gọi phương thức hữu ích ở mọi nơi trong dự án này và các lệnh khác, letcú pháp sẽ chỉ hữu ích trong rspec.

3. "Lợi ích" cho phép chúng ta dễ dàng bỏ qua các thay đổi thiết kế tốt

let()là tốt cho các phụ thuộc đắt tiền mà chúng ta không muốn tạo ra nhiều lần. Nó cũng kết hợp tốt vớisubject , cho phép bạn làm khô các cuộc gọi lặp lại với các phương thức đa đối số

Các phụ thuộc đắt tiền được lặp đi lặp lại nhiều lần và các phương thức có chữ ký lớn là cả hai điểm mà chúng ta có thể làm cho mã tốt hơn:

  • có lẽ tôi có thể giới thiệu một sự trừu tượng mới tách biệt sự phụ thuộc với phần còn lại của mã của tôi (điều đó có nghĩa là cần ít thử nghiệm hơn)
  • có thể mã đang thử nghiệm đang làm quá nhiều
  • có lẽ tôi cần tiêm các đối tượng thông minh hơn thay vì một danh sách dài các nguyên thủy
  • có lẽ tôi đã vi phạm nói-không-hỏi
  • có lẽ mã đắt tiền có thể được thực hiện nhanh hơn (hiếm hơn - hãy cẩn thận với việc tối ưu hóa sớm ở đây)

Trong tất cả các trường hợp này, tôi có thể giải quyết triệu chứng của các bài kiểm tra khó khăn bằng một loại thuốc làm dịu ma thuật rspec, hoặc tôi có thể thử giải quyết nguyên nhân. Tôi cảm thấy như tôi đã dành quá nhiều trong vài năm qua cho trước đây và bây giờ tôi muốn một số mã tốt hơn.

Để trả lời câu hỏi ban đầu: tôi không muốn, nhưng tôi vẫn sử dụng let. Tôi chủ yếu sử dụng nó để phù hợp với phong cách của các thành viên còn lại trong đội (có vẻ như hầu hết các lập trình viên Rails trên thế giới hiện đang chìm sâu vào ma thuật rspec của họ nên điều đó rất thường xuyên). Đôi khi tôi sử dụng nó khi tôi thêm một bài kiểm tra vào một số mã mà tôi không có quyền kiểm soát hoặc không có thời gian để cấu trúc lại để trừu tượng hóa tốt hơn: tức là khi lựa chọn duy nhất là thuốc giảm đau.


0

Tôi sử dụng letđể kiểm tra các phản hồi HTTP 404 của mình trong thông số API bằng cách sử dụng bối cảnh.

Để tạo tài nguyên, tôi sử dụng let!. Nhưng để lưu trữ định danh tài nguyên, tôi sử dụng let. Hãy xem nó trông như thế nào:

let!(:country)   { create(:country) }
let(:country_id) { country.id }
before           { get "api/countries/#{country_id}" }

it 'responds with HTTP 200' { should respond_with(200) }

context 'when the country does not exist' do
  let(:country_id) { -1 }
  it 'responds with HTTP 404' { should respond_with(404) }
end

Điều đó giữ cho các thông số kỹ thuật sạch và có thể đọc được.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.