Eigene Datenvalidierung in Rails: Felder müssen einzigartige Inhalte haben

12.03.2016 22:30

Die Validation von Benutzereingaben betrifft nicht nur das Aufdecken von Tippfehlern, sondern verhindert auch, dass unpassende Werte das System instabil machen. Eine durchdachte Datenvalidation kann zudem auch Spambots einen Strich durch die Rechnung machen.

Ruby on Rails bringt schon viele Hilfsmittel mit, um Modeldaten beim persistieren zu validieren. Es fehlt jedoch eine Möglichkeit, um beispielsweise zu überprüfen, ob der eingegebene Vor- und Nachname unterschiedlich ist. Auch nach längerer Suche habe ich niemanden in den Telefonbüchern entdecken können, welcher den selben Vor- und Nachname hat. Wenn ihr jemanden findet, meldet dich doch bei mir.

Testgetriebene Entwicklung

Ganz nach dem Prinzip, der testgetriebenen Entwicklung (TDD), wird erst einmal ein Test erstellt, welcher die Anforderungen definiert und später das Resultat überprüft. Aber Moment mal, hat nicht der dänische Autor von Rails, David Heinemeier Hansson, testgetriebene Entwicklung in seinem Blog für tot erklärt? Der Test beinhaltet ein Modell mit den beiden Feldern, Vor- und Nachname, welches als Mock-Objekt dient. So wie ich das verstehe, ist genau dieses Mock-Objekt einer der Gründe, weshalb der Rails-Erfinder TDD nicht mehr mag.

Ist dieser Validator in ein Rails-Projekt eingebettet, so wäre es sinnvoller einen Test für die jeweiligen Modelle zu schreiben, welche diese Validation voraussetzen. Diese Tests zu den Modellen sind kontextbezogen und überprüfen somit genauer die spezifische Anforderung an die Software. Extrahiert man jedoch diesen Validator, beispielsweise in ein separates Gem, so würde sich dieser Test mit dem Mock-Objekt wieder rechtfertigen. Ein abgeschlossenes Modul, welches vielleicht in mehreren Projekten verwendet wird, soll ja getestet sein.

require 'test_helper'

class ValuesNotEqualValidatorTestModel
  include ActiveModel::Validations
  validates :forename, :surname, values_not_equal: true
  attr_accessor :forename
  attr_accessor :surname
end

class ValuesNotEqualValidatorTest < ActiveSupport::TestCase
  test "if two equal values aren't valid" do
    model = ValuesNotEqualValidatorTestModel.new
    model.forename = 'friedrich'
    model.surname = 'friedrich'
    assert_not model.valid?
  end

  test "if two different values are valid" do
    model = ValuesNotEqualValidatorTestModel.new
    model.forename = 'fritz'
    model.surname = 'friedrich'
    assert model.valid?
  end
end

Eigener Validator

Nun aber zum Validator selbst. Dadurch, dass wir mehrere Felder überprüfen möchten, erweitern wir den EachValidator. Dieser ruft die Methode validate_each für jedes Feld auf. Ist ein Feld einem zuvor kontrollierten Feld gleich, so wird eine Fehlermeldung generiert, welche sich Rails i18n-Funktionalität bedient.

class ValuesNotEqualValidator < ActiveModel::EachValidator
  def validate(record)
    @past = Hash.new
    super
  end

  def validate_each(record, attribute, value)
    @past.each do |k, v|
      if v == value
        record.errors.add(attribute, I18n.t('errors.messages.should_not_be_equal_to', other: record.class.human_attribute_name(k)))
      end
    end
    @past[attribute] = value
  end
end