2012-04-11 5 views
5

Estoy creando una característica de importación que importa archivos CSV en varias tablas. Hice un módulo llamado CsvParser que analiza un archivo CSV y crea registros. Mis modelos que reciben las acciones de crear extienden el CsvParser. Realizan una llamada al CsvParser.create y pasan el orden de atributo correcto y un lambda opcional llamado value_parser. Esta lambda transforma los valores en un hash a un formato preffered.Prueba de una lambda

class Mutation < ActiveRecord::Base 
    extend CsvParser 

    def self.import_csv(csv_file) 
    attribute_order = %w[reg_nr receipt_date reference_number book_date is_credit sum balance description] 

    value_parser = lambda do |h| 
     h["is_credit"] = ((h["is_credit"] == 'B') if h["is_credit"].present?) 
     h["sum"] = -1 * h["sum"].to_f unless h["is_credit"] 
     return [h] 
    end 

    CsvParser.create(csv_file, self, attribute_order, value_parser)  
    end 
end 

La razón por la que estoy usando una lambda en lugar de cheques dentro del método CsvParser.create es debido a que el lambda es como una regla de negocio que pertenece a este modelo.

Mi pregunta es cómo debería probar esta lambda. ¿Debo probarlo en el modelo o en CsvParser? ¿Debo probar la lambda o el resultado de una matriz del método self.import? Tal vez debería hacer otra estructura de código?

Mi CsvParser se ve de la siguiente manera:

require "csv" 

module CsvParser 

    def self.create(csv_file, klass, attribute_order, value_parser = nil) 
    parsed_csv = CSV.parse(csv_file, col_sep: "|") 

    records = []  

    ActiveRecord::Base.transaction do 
     parsed_csv.each do |row| 
     record = Hash.new {|h, k| h[k] = []}  
     row.each_with_index do |value, index| 
      record[attribute_order[index]] = value 
     end 
     if value_parser.blank? 
      records << klass.create(record) 
     else 
      value_parser.call(record).each do |parsed_record| 
      records << klass.create(parsed_record) 
      end 
     end 
     end 
    end 
    return records 
    end 

end 

Estoy probando el propio módulo: requieren 'spec_helper'

describe CsvParser do 

    it "should create relations" do 
    file = File.new(Rails.root.join('spec/fixtures/files/importrelaties.txt')) 
    Relation.should_receive(:create).at_least(:once) 
    Relation.import_csv(file).should be_kind_of Array 
    end 

    it "should create mutations" do 
    file = File.new(Rails.root.join('spec/fixtures/files/importmutaties.txt')) 
    Mutation.should_receive(:create).at_least(:once)  
    Mutation.import_csv(file).should be_kind_of Array 
    end 

    it "should create strategies" do 
    file = File.new(Rails.root.join('spec/fixtures/files/importplan.txt')) 
    Strategy.should_receive(:create).at_least(:once) 
    Strategy.import_csv(file).should be_kind_of Array 
    end 

    it "should create reservations" do 
    file = File.new(Rails.root.join('spec/fixtures/files/importreservering.txt')) 
    Reservation.should_receive(:create).at_least(:once) 
    Reservation.import_csv(file).should be_kind_of Array 
    end 

end 

Respuesta

4

Algunas preguntas interesantes. Un par de notas:

  1. Probablemente no deba tener una devolución dentro de la lambda. Solo haz la última declaración [h].
  2. Si entiendo el código correctamente, la primera y la segunda líneas de su lambda son demasiado complicadas. Reducirlos para que sean más legibles y fáciles de refactor:

    h["is_credit"] = (h['is_credit'] == 'B') # I *think* that will do the same 
    h['sum'] = h['sum'].to_f # Your original code would have left this a string 
    h['sum'] *= -1 unless h['is_credit'] 
    
  3. Parece que tu lambda no depende de nada externo (aparte de h), por lo que pondría a prueba por separado. Incluso se puede hacer que sea una constante:

    class Mutation < ActiveRecord::Base 
        extend CsvParser # <== See point 5 below 
    
        PARSE_CREDIT_AND_SUM = lambda do |h| 
        h["is_credit"] = (h['is_credit'] == 'B') 
        h['sum'] = h['sum'].to_f 
        h['sum'] *= -1 unless h['is_credit'] 
        [h] 
        end 
    
  4. Sin conocer la razón de ser, es difícil decir donde se debe poner este código. Mi intuición es que no es el trabajo del analizador CSV (aunque un buen analizador puede detectar números en coma flotante y convertirlos de cadenas?) Mantenga su analizador CSV reutilizable. (Nota: Relectura, creo que usted mismo ha respondido esta pregunta: es lógica de negocios, vinculada al modelo. ¡Vaya con su instinto!)

  5. Por último, está definiendo y el método CsvParser.create. No es necesario extender CsvParser para obtener acceso a ella, aunque si tiene otras instalaciones en CsvParser, considerar la posibilidad CsvParser.create un método normal de módulo llamado algo así como create_from_csv_file

+0

Gracias por su respuesta exhaustiva, clara. Seguí tu consejo y pude probar la constante porque podría llamar a Class :: . Solo me pregunto cómo debería probar algo como esto cuando sería una variable en un método. –

+1

Si la lambda no se pudiera refactorizar, lo probaría como cualquier otro método. Si se trata de una lambda muy complicada, dependiendo de muchas variables locales, esto puede indicar que quiere ser su propia instancia. Como regla general, los métodos que son difíciles de probar necesitan un refactor. – user208769

+0

Hoy me di cuenta de que hay algo llamado objeto de método. Se puede usar de la misma manera que una lambda con la única diferencia de que puedo usar el código de un método en lugar de una lambda.Creo que es más limpio que definir un lambda como una constante. Entonces, en lugar de pasar el lambda como parámetro, ahora uso el método (: ) –