2010-09-10 15 views
9

Tôi đã phải vật lộn để thích nghi với cách tiếp cận chuẩn của mình cho mã .NET kiểm tra lái xe đến Ruby.Làm thế nào để di chuyển từ .NET-style TDD sang Ruby?

Như một ví dụ, tôi viết một lớp mà sẽ:

grab all *.markdown files from a directory 
    foreach file: 
    extract code samples from file 
    save code to file.cs in output directory 

Thông thường đối với .NET Tôi muốn kết thúc với một cái gì đó như:

class ExamplesToCode { 
    public ExamplesToCode(IFileFinder finder, IExampleToCodeConverter converter) { ... } 
    public void Convert(string exampleDir, string targetDir) { ... } 
} 

Trong thử nghiệm của tôi (bằng văn bản đầu tiên) , Tôi sẽ giả lập công cụ tìm và chuyển đổi. Sau đó, tôi sẽ khai báo số finder.FindFiles("*.markdown") để trả lại số ["file1", "file2"] và kiểm tra converter.Convert("file1", targetDir)converter.Convert("file2", targetDir) đã được gọi.

Trường hợp tôi đấu tranh áp dụng điều này với Ruby là Ruby có xu hướng sử dụng các khối và trình lặp nội bộ (ví dụ: array.each { |x| puts x }) và bao gồm các mô-đun trên trình tạo hàm tạo. Tôi không chắc chắn về làm thế nào để kiểm tra đơn vị mã trong những trường hợp (mà không cần thiết lập một bài kiểm tra tích hợp đầy đủ), và cách tiếp cận NET chỉ có vẻ vô cùng un-rubyish; nó dường như chống lại cách Ruby tự nhiên hoạt động.

Bất kỳ đề xuất nào về cách thực hiện điều này theo cách của Ruby? Một ví dụ về một thử nghiệm Ruby cho ví dụ này sẽ là tuyệt vời.

Trả lời

2

Bạn có thể có một thử nghiệm rất nhiên mà đi một cái gì đó như thế này:

class ExamplesToCodeTest < Test::Unit::TestCase 
    def test_convert 
    # have some example markdown files in a fixtures directory 
    ExamplesToCode.convert("test/fixtures/*.markdown") 
    assert_equal expected_output_1, File.read("test/output/file_1.cs") 
    assert_equal expected_output_2, File.read("test/output/file_2.cs") 
    assert_equal expected_output_3, File.read("test/output/file_3.cs") 
    end 
    private 
    def expected_output_1 
     "... expected stuff here ..." 
    end 
    def expected_output_2 
     "... expected stuff here ..." 
    end 
    def expected_output_3 
     "... expected stuff here ..." 
    end 
end 

Tôi cho rằng sẽ làm cho một thử nghiệm hội nhập khá, nhưng đó không phải những gì tôi thực sự thích, tôi muốn có mã của tôi trong cắn khối -Kích thước

Trước tiên tôi muốn tạo ra một lớp có thể xử lý phân tích một tập markdown, ví dụ:

class MarkdownReaderTest < Test::Unit::TestCase 
    def test_read_code_sample_1 
    reader = MarkdownReader.new 
    code_sample = reader.read("fixtures/code_sample_1.markdown") 
    # or maybe something like this: 
    # code_sample = reader.parse(File.read("fixtures/code_sample_1.markdown")) 
    # if you want the reader to just be a parser... 
    assert_equal code_sample_1, code_sample 
    end 
    # ... repeat for other types of code samples ... 
    private 
    def code_sample_1 
     "text of code sample 1 here..." 
    end 
end 

Bây giờ tất cả các mã để đọc và phân tích các file markdown là trong Markd lớp ownReader. Bây giờ nếu chúng ta không muốn phải thực sự viết các tập tin bạn có thể nhận được ưa thích và thực hiện một số chế nhạo với RR hoặc Mocha hoặc một cái gì đó (tôi đang sử dụng rr đây):

class CodeSampleWriter < Test::Unit::TestCase 
    include RR::Adapters::TestUnit 
    def test_write_code_sample 
    # assuming CodeSampleWriter class is using the File.write()... 
    any_instance_of(File) do |f| 
     mock(f).write(code_sample_text) { true } 
    end 
    writer = CodeSampleWriter.new 
    writer.write(code_sample_text) 
    end 
    private 
    def code_sample_text 
     "... code sample text here ..." 
    end 
end 

Bây giờ giả sử lớp ExamplesToCode sử dụng Các lớp MarkdownReader và CodeSampleWriter, bạn lại có thể sử dụng các đối tượng giả với RR như sau:

class ExamplesToCodeTest < Test::Unit::TestCase 
    include RR::Adapters::TestUnit 
    def test_convert 
    # mock the dir, so we don't have to have an actual dir with files... 
    mock(Dir).glob("*.markdown") { markdown_file_paths } 
    # mock the reader, so we don't actually read files... 
    any_instance_of(MarkdownReader) do |reader| 
     mock(reader).read("file1.markdown") { code_sample_1 } 
     mock(reader).read("file2.markdown") { code_sample_1 } 
     mock(reader).read("file3.markdown") { code_sample_1 } 
    end 
    # mock the writer, so we don't actually write files... 
    any_instance_of(CodeSampleWriter) do |writer| 
     mock(writer).write_code_sample(code_sample_1) { true } 
     mock(writer).write_code_sample(code_sample_2) { true } 
     mock(writer).write_code_sample(code_sample_3) { true } 
    end 
    # now that the mocks are mocked, it's go time! 
    ExamplesToCode.new.convert("*.markdown") 
    end 
    private 
    def markdown_file_paths 
     ["file1.markdown", "file2.markdown", "file3.markdown"] 
    end 
    def code_sample_1; "... contents of file 1 ..."; end 
    def code_sample_2; "... contents of file 2 ..."; end 
    def code_sample_3; "... contents of file 3 ..."; end 
end 

Hy vọng điều này sẽ cho bạn một số ý tưởng về cách tiếp cận thử nghiệm trong Ruby. Không phải là viêm, nhưng đối với hầu hết các phần, tiêm phụ thuộc không phải là một cái gì đó nhìn thấy hoặc được sử dụng trong thế giới Ruby - nó thường cho biết thêm rất nhiều chi phí. Mocking/Doubles thường là một lựa chọn tốt hơn để thử nghiệm.

0

Ngay cả trong ruby ​​chỉ có hai cách để tách mã này: DI hoặc định vị dịch vụ. Trong số hai tôi vẫn thích DI như bạn đã mô tả.

Tôi không chắc chắn về thành ngữ ruby ​​nhưng tôi nghi ngờ rằng họ sẽ không bận tâm với sự trừu tượng IFileFinder, thay vì trực tiếp gọi Dir ["*. Makrkdown"] và sau đó viết lại trong bài kiểm tra.

+0

Bằng cách "viết lại nội dung đó trong bài kiểm tra", bạn có ý nghĩa là bản vá Dir-chan không? –

2

Trước khi trả lời câu hỏi về cách cung cấp cách thức để thực hiện việc này trong Ruby, tôi muốn xóa một số hiểu lầm. Trước hết, tôi sẽ không nói rằng có một "cách Ruby" của các bài kiểm tra những thứ như thế này hơn là có một cách nghiêm ngặt để kiểm tra một cái gì đó như thế này trong NET (mà, thừa nhận tôi đã không được sử dụng trong nhiều năm). . Người ta có thể sử dụng phương pháp tiếp cận dựa trên tương tác (hoặc chế nhạo) hoặc, như bạn đã nói, sử dụng phương pháp tiếp cận dựa trên trạng thái hơn bằng cách tạo một bài kiểm tra tích hợp thực hiện cả ba lớp cùng một lúc. Sự cân bằng giữa hai cách tiếp cận mà tôi nghĩ là ngôn ngữ bất khả tri. Ruby hasmanymockingframeworks sẽ cho phép bạn thực hiện phương pháp tiếp cận dựa trên tương tác nếu đó là điều bạn thấy thoải mái nhất. (Tôi thường sử dụng tàu có tàu RSpec.)

Thứ hai, tôi không nghĩ rằng "bao gồm các mô-đun trong quá trình tiêm xây dựng" là một tuyên bố chính xác. Mô-đun là một công cụ bổ sung có sẵn cho bạn trong Ruby nhưng chúng không có nghĩa là thay thế thiết kế OO tốt bằng thành phần đối tượng. Tôi chuyển các phụ thuộc vào các bộ khởi tạo của tôi mọi lúc trong Ruby vì nó làm cho chúng dễ kiểm tra và có thể tái sử dụng được nhiều hơn. Tôi thường sẽ mặc định phụ thuộc trong danh sách đối số tuy nhiên như vậy def initialize(converter=CodeConverter.new).

Bây giờ, để trả lời câu hỏi của bạn. Những gì liammclennan nói về việc sử dụng Dir::[] là chính xác - finder là không cần thiết. Vì vậy, câu hỏi là, làm thế nào để bạn viết các bài kiểm tra cho các phương pháp gọi Dir::[]? Bạn có ba tùy chọn: 1) Sử dụng một trong các thư viện nhại được đề cập ở trên và khai báo Dir::[] (đây là cách tiếp cận đơn giản và dễ dàng), 2) Ghi tệp vào đĩa và xác minh chúng được đọc (ick) hoặc 3) Sử dụng thư viện như FakeFS để ngăn chặn đĩa IO nhưng vẫn cho phép bạn viết một thử nghiệm tìm kiếm tự nhiên.Ví dụ sau đây là một cách có thể viết/thử nghiệm này (sử dụng RSpec và FakeFS) mà có phần song song với thiết kế ban đầu của bạn:

class CodeExtractor 

    def self.extract_dir(example_dir, target_dir) 
    Dir[example_dir + "/*.md"].each do |filename| 
     self.extract(filename, target_dir) 
    end 
    end 

    def self.extract(*args) 
    self.new(*args).extract 
    end 

    def extract(filename, target_dir) 
    # ... 
    end 
end 

# The spec... 
require 'fakefs/spec_helpers' 
describe CodeExtractor do 
    include FakeFS::SpecHelpers 

    describe '::extract_dir' do 
    it "extracts each markdown file in the provided example dir" do 
     FileUtils.touch(["foo.md", "bar.md"]) 
     CodeExtractor.should_receive(:extract).with(Dir.pwd + "/foo.md","/target") 
     CodeExtractor.should_receive(:extract).with(Dir.pwd + "/bar.md","/target") 
     CodeExtractor.extract_dir(Dir.pwd, "/target") 
    end 
    end 

    describe '#extract' do 
    it "blah blah blah" do 
     # ... 
    end 
    end 
end 

Tất nhiên, có những vấn đề nếu một xét nghiệm như vậy thêm đủ giá trị để xứng đáng đó là sự tồn tại. Tôi không nghĩ rằng tôi sẽ đi vào đó mặc dù .... Nếu bạn quyết định sử dụng FakeFS lưu ý rằng stacktraces từ lỗi có thể không hữu ích kể từ khi FS được giả mạo khi RSpec cố gắng để có được số dòng FS không tồn tại. :) Trùng hợp ngẫu nhiên, tôi có một số mã đọc và phân tích các trang đánh dấu trên github. specs có thể phục vụ thêm examples cách bạn có thể tiếp cận thử nghiệm những thứ như thế này trong Ruby. HTH, và chúc may mắn.

+0

Nhận xét của người đăng ban đầu về "bao gồm các mô-đun về tiêm phụ thuộc" được dựa trên bài viết này: http://fabiokung.com/2010/05/06/ruby-and-dependency-injection-in-a-dynamic-world/ Vì vậy mặc dù bạn vẫn có thể thực hiện DI thông qua hàm tạo, nhưng anh ta đã thấy công đức trong tùy chọn mô-đun được trình bày trong bài viết. – mkmurray

+0

Cảm ơn Ben. Rất hữu ích. Trong khi tôi viết "cách Ruby", tôi thực sự có nghĩa là "một cách Ruby". Tôi nhận ra có rất nhiều cách để làm điều đó, nhưng một số cách phù hợp với nền tảng khác nhau hơn những người khác. –

0

Điều thú vị đủ, Derick Bailey của LosTechies.com chỉ là bây giờ đăng tải một bài viết trên blog về khuôn mã để có thể kiểm chứng dễ dàng hơn:

http://www.lostechies.com/blogs/derickbailey/archive/2010/09/10/design-and-testability.aspx

Derick đề cập rằng trong Ruby bạn không cần phải cố gắng hết sức như các ngôn ngữ khác như C# để mã của bạn có thể kiểm tra được. Vì vậy, có lẽ câu trả lời là luồng công việc giống như BDD từ trên xuống mà bạn đã chọn từ J.P. Boodhoo's Nothing Nhưng .NET bootcamp http://jpboodhoo.com/training.oo không áp dụng giống như cách bạn làm trong C#. Tương tự như vậy sẽ đi cho thử nghiệm kata đảo chữ số nhỏ của tôi Tôi đã làm trên blog của tôi một số tháng trở lại, nơi tôi khám phá kỹ thuật tương tự trong C# http://murrayon.net/2009/11/anagram-code-kata-bdd-mspec.html. Tôi đang cố gắng tìm ra điều này có nghĩa là ... có lẽ bạn cần phải loại bỏ ý tưởng về các giao diện, bởi vì trong Ruby bạn nên làm đa hình thông qua bố cục và không phải thừa kế.

+0

Nhân tiện, các bình luận trên bài đăng trên blog của Derick đang hình thành một cuộc thảo luận khá thú vị. – mkmurray

1

Trong số tất cả mã giả đó, điều duy nhất thực sự làm tôi lo lắng là "trích xuất các mẫu mã từ tệp". Đọc các tập tin từ một thư mục là tầm thường, lưu một tập tin là tầm thường. Bất kể khung kiểm thử tôi dành phần lớn thời gian của mình tập trung vào bit phân tích cú pháp.

Để thử nghiệm trực tiếp, tôi muốn nhúng các đoạn thẳng vào trường hợp thử nghiệm:

# RSPec 
describe "simple snippet" do 
    before(:each) do 
    snippet =<<SNIPPET 
increment a variable 
= code 
x = x + 1 
SNIPPET 
    @snippets = ExamplesToCode.parse(snippet) 
    end 
    it "should capture the snippet" do 
    @snippets.should include("x = x + 1\n") 
    end 
    it "should ignore the comment" do 
    @snippets.any? {|snip| snip =~ /increment a variable}.should be_nil 
    end 
end 

Ah, tôi thấy một sự thay đổi tinh tế tôi làm trong khi viết các bài kiểm tra: ExamplesToCode.parse của tôi() trả về một mảng (hoặc vùng chứa có thể lặp lại khác), để nó có thể được kiểm tra ngoài việc lặp lại chính nó.

Các vấn đề liên quan