2010-09-10 9 views
9

Starałem się dostosować moje standardowe podejście do testowania kodu .NET do Rubiego.Jak przejść z TDD .NET do Ruby?

Jako przykład, piszę klasy, która będzie:

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

Normalnie NET Chciałbym skończyć z czymś takim:

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

W moim teście (napisany pierwszy) , Znajdę fałszywą wyszukiwarkę i konwerter. Następnie zgasiłbym finder.FindFiles("*.markdown"), aby powrócić, powiedz ["file1", "file2"], i sprawdź, czy został wywołany converter.Convert("file1", targetDir) i converter.Convert("file2", targetDir).

Gdzie walczę, stosując Ruby, to to, że Ruby korzysta z bloków i wewnętrznych iteratorów (np. array.each { |x| puts x }), włączając moduły nad wtryskiem konstruktora. Nie jestem pewien w jaki sposób testować kod jednostkowy w tych przypadkach (bez ustanawiania pełnego testu integracji), a podejście .NET wydaje się niewiarygodnie nie rubinowe; zdaje się walczyć tak, jak Ruby działa naturalnie.

Jakieś sugestie, jak to zrobić w sposób Rubinowy? Przykład testu Ruby dla tego przykładu byłby świetny.

Odpowiedz

2

Można mieć bardzo oczywiście test, który wygląda mniej więcej tak:

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 

Przypuszczam, że byłoby zrobić test przyzwoitego integracyjnej, ale to nie to, co naprawdę lubię, lubię mieć mojego kodu w ukąszeniu -size kawałki

pierwsze chciałbym utworzyć klasę, która może obsłużyć parsowania pliku markdown, np

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 

teraz cały kod czytać i analizować pliki przecenowych jest w markd klasa ownReader. Teraz, jeśli nie chcemy mieć faktycznie zapisu plików można uzyskać fantazyjny i zrobić wyśmianie z RR lub Mocha czy coś (używam rr tutaj):

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 

teraz zakładając klasa ExamplesToCode używa klasy MarkdownReader i CodeSampleWriter, można ponownie użyć makiety obiektów z RR tak:

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 

Mam nadzieję, że to daje pewne pomysły, jak podejść do testu w Ruby. Nie jest zapalne, ale w przeważającej części zastrzyk zależny nie jest czymś widzianym lub używanym w świecie Ruby - generalnie dodaje dużo narzutów. Szyderstwo/Podwójne są zwykle znacznie lepszą opcją do testowania.

0

Nawet w rubinach istnieją tylko dwa sposoby oddzielenia tego kodu: DI lub lokalizator usługi. Z tych dwóch wciąż wolę DI, jak opisałeś.

Nie jestem pewien rubio idiomów, ale podejrzewam, że nie zawracają sobie głowy abstrakcją IFileFinder, zamiast tego bezpośrednio wywołują Dir ["*. Makrkdown"], a następnie przepisują to w teście.

+0

Przez "przepisanie tego w teście" masz na myśli łatanie małp? –

2

Zanim odpowiem na pytanie, jak to zrobić w Ruby, chciałbym wyjaśnić kilka nieporozumień.

Po pierwsze, nie powiedziałbym, że istnieje "sposób Rubinowy" testów takich rzeczy, podobnie jak nie ma ścisłego sposobu testowania czegoś takiego w .NET (co, wprawdzie nie używam od lat) . Można zastosować podejście oparte na interakcji (szydercze) lub, jak już powiedziałeś, przyjąć bardziej podejście oparte na stanie, tworząc test integracji, który wykonuje wszystkie trzy klasy jednocześnie. Kompromisy między tymi dwoma podejściami są, moim zdaniem, agnostyczne. Ruby hasmanymockingframeworks który pozwoliłby ci zastosować podejście oparte na interakcji, jeśli jest to najbardziej wygodne. (Zazwyczaj używam tego, który jest dostarczany z RSpec.)

Po drugie, nie uważam, że "w tym moduły nad iniekcją konstruktora" jest dokładnym stwierdzeniem. Moduły są dodatkowym narzędziem dostępnym w Ruby, ale nie zastępują dobrego projektu OO kompozycją obiektów. Przez cały czas przekazuję zależności do moich inicjalizatorów w Ruby, ponieważ ułatwia to ich testowanie i wielokrotne użycie. Generalnie domyślnie będę zależał od listy argumentów, ale tak jak def initialize(converter=CodeConverter.new).

Teraz, aby odpowiedzieć na twoje pytanie. To, co liammclennan powiedział o używaniu Dir::[], jest poprawne - nie jest potrzebne finder. Tak więc pytanie brzmi: jak pisać testy dla metod, które wywołują Dir::[]? Masz trzy możliwości: 1) Użyj jednej z wyżej wymienionych bibliotek szyderczych i kodu pośredniczącego Dir::[] (jest to prosta i łatwa metoda), 2) Zapisz pliki na dysku i sprawdź, czy są one przeczytane (ick), lub 3) Użyj biblioteki jak FakeFS, aby zapobiec IO dysku, ale nadal pozwala napisać naturalny wygląd testu.Poniższy przykład jest jeden możliwy sposób pisania/testowania to (używając RSpec i FakeFS), który jest nieco równolegle do pierwotnego projektu:

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 

oczywiście, nie jest to kwestia, czy taki test dodaje tyle wartość zasługuje na jego istnienie. Nie sądzę, że wpadnę na to .... Jeśli zdecydujesz się użyć FakeFS, pamiętaj, że stosy błędów mogą nie być pomocne, ponieważ FS jest fałszywe, gdy RSpec próbuje wyłączyć numer linii nieistniejący FS. :) Zbiegiem okoliczności, mam trochę kodu, który czyta i parsuje slajdy Markdown na github. The specs może służyć jako dalsze examples, jak możesz podejść do testowania takich rzeczy w Ruby. HTH, i powodzenia.

+0

Komentarz do oryginalnego plakatu na temat "włączenia modułów do iniekcji zależnej" jest oparty na tym artykule: http://fabiokung.com/2010/05/06/ruby-and-dependency-injection-in-a-dynamic-world/ chociaż nadal można wykonać DI za pomocą konstruktora, zauważył on zasługę w opcji modułu przedstawionej w tym artykule. – mkmurray

+0

Dzięki Ben. Bardzo pomocne. Kiedy pisałem "Ruby Way", naprawdę miałem na myśli "Ruby Way". Rozumiem, że jest na to wiele sposobów, ale niektóre sposoby pasują do różnych platform bardziej niż inne. –

0

Co ciekawe, Derick Bailey z LosTechies.com właśnie pisał na blogu o formowanie kod będzie łatwiej sprawdzalne:

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

Derick wspomina, że ​​Ruby nie trzeba spróbować tak ciężko jak inne języki, takie jak C#, aby twój kod był testowalny.

Być może odpowiedź brzmi, że Twój workflow z pozycją od dołu do BDD, który wybrałeś z Buphoo's Nothing But .NET bootcamp http://jpboodhoo.com/training.oo, nie ma zastosowania w taki sam sposób, jak w C#. To samo dotyczyłoby mojego małego eksperymentu z kata kodem anagramu, który zrobiłem na moim blogu kilka miesięcy temu, gdzie badałem podobne techniki w C# http://murrayon.net/2009/11/anagram-code-kata-bdd-mspec.html. Próbuję dowiedzieć się, co by to oznaczało ... może musisz zepsuć ideę interfejsów, ponieważ w Ruby powinieneś robić polimorfizm poprzez kompozycję, a nie dziedziczenie.

+0

Nawiasem mówiąc, komentarze do posta na blogu Derick'a kształtują się na całkiem interesującą dyskusję. – mkmurray

1

Spośród wszystkich pseudokod, jedyną rzeczą, która naprawdę mnie martwi, jest "wyodrębnianie próbek kodu z pliku". Czytanie plików z katalogu jest banalne, a zapisanie pliku jest banalne. Niezależnie od ram testowych spędziłem większość czasu, koncentrując się na analizie.

Do bezpośredniego badania, chciałbym osadzić fragmenty bezpośrednio do przypadku testowego:

# 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 

Ach, widzę kolejną zmianę I subtelnie wykonane podczas pisania testu: mój ExamplesToCode.parse() zwraca tablicę (lub inny iteracyjny pojemnik), dzięki czemu można go przetestować niezależnie od samej iteracji.