mardi 9 février 2016

How can I test these RSS parsing service objects?

I have some service objects that use Nokogiri to make AR instances. I created a rake task so that I can update the instances with a cron job. What I want to test is if it's adding items that weren't there before, ie:

  • Create an Importer with a url of spec/fixtures/feed.xml, feed.xml having 10 items.
  • Expect Show.count == 1 and Episode.count == 10
  • Edit spec/fixtures/feed.xml to have 11 items
  • Invoke rake task
  • Expect Show.count == 1 and Episode.count == 11

How could I test this in RSpec, or modify my code to be more testable?

# 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

Aucun commentaire:

Enregistrer un commentaire