如何测试这些RSS解析服务对象?

时间:2016-02-09 19:56:41

标签: ruby-on-rails ruby testing service rspec

我有一些使用Nokogiri制作AR实例的服务对象。我创建了一个rake任务,以便我可以使用cron作业更新实例。我想测试的是,它是否正在添加以前不存在的项目,即:

  • 使用Importer url创建spec/fixtures/feed.xml,feed.xml包含10个项目。
  • 期待Show.count == 1Episode.count == 10
  • 修改spec/fixtures/feed.xml以包含11个项目
  • 调用rake任务
  • 期待Show.count == 1Episode.count == 11

我如何在RSpec中测试它,或者修改我的代码以使其更易于测试?

# models/importer.rb
class Importer < ActiveRecord::Base
  after_create :parse_importer

  validates :title, presence: true
  validates :url, presence: true
  validates :feed_format, presence: true

  private

  def parse_importer
    Parser.new(self)
  end
end

# models/show.rb
class Show < ActiveRecord::Base
  validates :title, presence: true
  validates :title, uniqueness: true

  has_many :episodes

  attr_accessor :entries
end

# models/episode.rb
class Episode < ActiveRecord::Base
  validates :title, presence: true
  validates :title, uniqueness: true

  belongs_to :show
end

#lib/tasks/admin.rake
namespace :admin do
  desc "Checks all Importer URLs for new items."
  task refresh: :environment do
    @importers = Importer.all

    @importers.each do |importer|
      Parser.new(importer)
    end
  end
end

# services/parser.rb
class Parser
  def initialize(importer)
    feed = Feed.new(importer)
    show = Show.where(rss_link: importer.url).first

    if show # add new episodes
      new_episodes = Itunes::Channel.refresh(feed.origin)

      new_episodes.each do |new_episode|
        show.episodes.create feed.episode(new_episode)
      end
    else # create a show and its episodes
      new_show = Show.new(feed.show) if (feed && feed.show)

      if (new_show.save && new_show.entries.any?)
        new_show.entries.each do |entry|
          new_show.episodes.create feed.episode(entry)
        end
      end
    end
  end
end

# services/feed.rb
class Feed
  require "nokogiri"
  require "open-uri"
  require "formats/itunes"

  attr_reader :params, :origin, :show, :episode

  def initialize(params)
    @params = params
  end

  def origin
    @origin = Nokogiri::XML(open(params[:url]))
  end

  def format
    @format = params[:feed_format]
  end

  def show
    case format
      when "iTunes"
        Itunes::Channel.fresh(origin)
    end
  end

  def episode(entry)
    @entry = entry

    case format
      when "iTunes"
        Itunes::Item.fresh(@entry)
    end
  end
end

# services/formats/itunes.rb
class Itunes
  class Channel
    def initialize(origin)
      @origin = origin
    end

    def title
      @origin.xpath("//channel/title").text
    end

    def description
      @origin.xpath("//channel/description").text
    end

    def summary
      @origin.xpath("//channel/*[name()='itunes:summary']").text
    end

    def subtitle
      @origin.xpath("//channel/*[name()='itunes:subtitle']/text()").text
    end

    def rss_link
      @origin.xpath("//channel/*[name()='atom:link']/@href").text
    end

    def main_link
      @origin.xpath("//channel/link/text()").text
    end

    def docs_link
      @origin.xpath("//channel/docs/text()").text
    end

    def release
      @origin.xpath("//channel/pubDate/text()").text
    end

    def image
      @origin.xpath("//channel/image/url/text()").text
    end

    def language
      @origin.xpath("//channel/language/text()").text
    end

    def keywords
      keywords_array(@origin)
    end

    def categories
      category_array(@origin)
    end

    def explicit
      explicit_check(@origin)
    end

    def entries
      entry_array(@origin)
    end

    def self.fresh(origin)
      @show = Itunes::Channel.new origin

      return {
        description: @show.description,
        release: @show.release,
        explicit: @show.explicit,
        language: @show.language,
        title: @show.title,
        summary: @show.summary,
        subtitle: @show.subtitle,
        image: @show.image,
        rss_link: @show.rss_link,
        main_link: @show.main_link,
        docs_link: @show.docs_link,
        categories: @show.categories,
        keywords: @show.keywords,
        entries: @show.entries
      }
    end

    def self.refresh(origin)
      @show = Itunes::Channel.new origin
      return @show.entries
    end

    private

    def category_array(channel)
      arr = []
      channel.xpath("//channel/*[name()='itunes:category']/@text").each do |category|
        arr.push(category.to_s)
      end
      return arr
    end

    def explicit_check(channel)
      string = channel.xpath("//channel/*[name()='itunes:explicit']").text

      if string === "yes" || string === "Yes"
        true
      else
        false
      end
    end

    def keywords_array(channel)
      keywords = channel.xpath("//channel/*[name()='itunes:keywords']/text()").text
      arr = keywords.split(",")
      return arr
    end

    def entry_array(channel)
      arr = []
      channel.xpath("//item").each do |item|
        arr.push(item)
      end
      return arr
    end
  end

  class Item
    def initialize(origin)
      @origin = origin
    end

    def description
      @origin.xpath("*[name()='itunes:subtitle']").text
    end

    def release
      @origin.xpath("pubDate").text
    end

    def image
      @origin.xpath("*[name()='itunes:image']/@href").text
    end

    def explicit
      explicit_check(@origin)
    end

    def duration
      @origin.xpath("*[name()='itunes:duration']").text
    end

    def title
      @origin.xpath("title").text
    end

    def enclosure_url
      @origin.xpath("enclosure/@url").text
    end

    def enclosure_length
      @origin.xpath("enclosure/@length").text
    end

    def enclosure_type
      @origin.xpath("enclosure/@type").text
    end

    def keywords
      keywords_array(@origin.xpath("*[name()='itunes:keywords']").text)
    end

    def self.fresh(entry)
      @episode = Itunes::Item.new entry

      return {
        description: @episode.description,
        release: @episode.release,
        image: @episode.image,
        explicit: @episode.explicit,
        duration: @episode.duration,
        title: @episode.title,
        enclosure_url: @episode.enclosure_url,
        enclosure_length: @episode.enclosure_length,
        enclosure_type: @episode.enclosure_type,
        keywords: @episode.keywords
      }
    end

    private

    def explicit_check(item)
      string = item.xpath("*[name()='itunes:explicit']").text

      if string === "yes" || string === "Yes"
        true
      else
        false
      end
    end

    def keywords_array(item)
      keywords = item.split(",")
      return keywords
    end
  end
end

1 个答案:

答案 0 :(得分:0)

在其他任何事情之前,对您使用服务对象有好处!我已经大量使用这种方法,并且在许多情况下发现PORO比胖模型更可取。

您对测试感兴趣的行为似乎包含在Parser.initialize中。

首先,我为Parser创建一个名为parse的类方法。 IMO,Parser.parse(importer)更清楚Parser正在做什么而不是Parser.new(importer)。所以,它可能看起来像:

#services/parser.rb
class Parser
  class << self

    def parse(importer)
      @importer = importer
      @feed = Feed.new(importer)

      if @show = Show.where(rss_link: importer.url).first
        create_new_episodes Itunes::Channel.refresh(@feed.origin)
      else
        create_show_and_episodes
      end
    end # parse

  end
end

然后添加create_new_episodescreate_show_and_episodes类方法。

#services/parser.rb
class Parser
  class << self

    def parse(importer)
      @importer = importer
      @feed     = Feed.new(importer)

      if @show = Show.where(rss_link: @importer.url).first
        create_new_episodes Itunes::Channel.refresh(@feed.origin)
      else
        create_show_and_episodes
      end
    end # parse

    def create_new_episodes(new_episodes)
      new_episodes.each do |new_episode|
        @show.episodes.create @feed.episode(new_episode)
      end
    end # create_new_episodes

    def create_show_and_episodes
      new_show = Show.new(@feed.show) if (@feed && @feed.show)

      if (new_show.save && new_show.entries.any?)
        new_show.entries.each do |entry|
          new_show.episodes.create @feed.episode(entry)
        end
      end
    end # create_show_and_episodes

  end
end

现在您有一个Parser.create_new_episodes方法可以独立测试。因此,您的测试可能类似于:

require 'rspec_helper'

describe Parser do
  describe '.create_new_episodes' do
    context 'when an initial parse has been completed' do 
      before(:each) do
        first_file = Nokogiri::XML(open('spec/fixtures/feed_1.xml'))
        @second_file = Nokogiri::XML(open('spec/fixtures/feed_2.xml'))
        Parser.create_show_and_episodes first_file
      end
      it 'changes Episodes.count by 1' do
        expect{Parser.create_new_episodes(@second_file)}.to change{Episodes.count}.by(1)
      end
      it 'changes Show.count by 0' do
        expect{Parser.create_new_episodes(@second_file)}.to change{Show.count}.by(0)
      end
    end
  end
end

当然,您在feed_1.xml目录中需要feed_2.xmlspec\fixtures

对任何错别字道歉。而且,我没有运行代码。所以,可能是马车。希望它有所帮助。