Search code examples
ruby-on-railsrubyrspectddbdd

Build up help for TDD/BDD with RSpec


For about 2 weeks ago i've started learning Ruby, i've made an WebParser / Page WordCounter using 'open-uri' and 'nokogiri'. So i just run up the terminal with 'ruby counter.rb http://test.com word' and i and up with the number of matches of that word, case insensitive so i can grab everything.

SO here I am learning about RSpec, TDD, BDD and all this stuff, and i would like to know how my code could be constructed using RSpec examples and expectations. Ive already read all the documentation, i'm building simples examples to test, etc.

I would like to know if there is anyone that could build my code into RSpec examples and expectations, so I can study what you've done and how you've done.

Here is my code:

require 'open-uri'
require 'nokogiri'

class Counter   

    def initialize(url)
      @url = url
    end

    def count(word, url)
      doc = Nokogiri::HTML(open(url))
      doc.css('head').remove
      doc.text.scan(/#{word}/i).size
    end
end

url, word = ARGV
puts "Found: #{Counter.new(url).count(word, url)} matches."

Hope someone could help me, i'm really into ruby and found this RSpec amazing,

Thanks guys, i'll be studying and waiting!


Solution

  • there's an rspec --init command that will create your boilerplate.

    Once you've done this, open spec_helper.rb and require your code file.

    By the way, it's a little odd that your initialize accepts a url and assigns it to an instance variable, but the count method takes a url as an argument.

    So assuming it's refactored to this:

    attr_reader :url
    def initialize(url)
      @url = url
    end
    
    def count(word)
      doc = Nokogiri::HTML(open(url)) # this uses the attr_reader
      doc.css('head').remove
      doc.text.scan(/#{word}/i).size
    end
    

    Then you can write a test case like this (this is not a comprehensive coverage, just an example):

    describe "Counter" do
      let(:url) { "http://some_url" }
      let(:counter) { Counter.new url }
      it "counts words" do
        expect(counter.count("foo")).to(
          eq("<whatever the expected result is>")
        )
      end
    end
    

    Using let to set variables is optional. You could also set the variables inside the it ... do block, but you'd have to repeat it for each case.

    In addition to .to you have .not_to, and there are many other helpful methods than eq. I recommend reading the RSpec matcher docs to familiarize yourself with these.

    It's also worth mentioning that this test case will make an HTTP call which is sometimes desired, and sometimes not. For example if you have many cases and want to run them quickly, then removing HTTP calls will be beneficial. But doing this means that you are no longer actually testing the state of the url. What if the markup changes? Unless your test case actually makes the HTTP call, you won't know for sure.

    Still, it's good to know how to remove the HTTP call since the underlying concept ("mocking" or "stubbing") has a lot of uses. Something like this:

      it "counts words" do
        mock_html = <<-HTML
          <!doctype html>
          <html lang='en'>
            <head></head>
            <body>foo</body>
          </html>
        HTML
        expect(Object).to(
          receive(:open).with(any_args).at_least(1).times.and_return(mock_html)
        )
        expect(counter.count("foo")).to eq(1)
        expect(counter.count("bar")).to eq(0)
      end
    

    any_args is a special term you can use when stubbing methods. You could also use url here since you know what the passed argument is going to be.

    For more info, again I'll refer you to RSpec's docs, this time the ones about mocking / stubbing.

    Generally you want to focus mostly on input/output of functions. Sometimes you will want to check that another method is called (you'd use mock/stub for this) but it's probably not expected of you to test every single line of code.