Vollständige Dependent-Einstellungen in Rails-Models testen

Ein selten, aber leider regelmäßig wiederkehrendes Problem sind Fremdschlüsselbeziehungen beim Löschen. In Rails muss auf Seite des Schlüsselziels definiert werden, ob ein Fremdschlüssel auf null gesetzt werden darf oder ob das ganze Model gelöscht werden muss. Vergisst man diese Konfiguration wirft die Datenbank beim Löschen einen Fehler. Um dies zu vermeiden möchte ich alle Fremdschlüssel, die auf eine Model-Tabelle zeigen, überprüfen, ob sie entsprechend konfiguriert sind. Hierfür verwende ich ein shared Example in Rspec.

Fremdschlüssel finden

Die Schlüssel für das beschriebene Model lassen sich in zumindest mit Postgres folgendermaßen auflisten:

def foreign_keys(to_table)
  ActiveRecord::Base.connection.tables.map do |table_name|
    ActiveRecord::Base.connection.foreign_keys(table_name).select{ |index| index.to_table == to_table }
  end.flatten
end

Konfigurierte Assoziationen finden

Die Assoziationen eines Models finden wir mit folgendem Ansatz. Wir ignorieren Assoziationen mit Scopes, da hier nicht ohne weiteres festgestellt werden kann, ob diese alle möglichen Verbindungen abdecken. Auch beschränken wir uns auf Has-many- und Has-one-Beziehungen, da diese zumindest aktuell die einzig relevanten sind. Sollte hier etwas fehlen ist dies auch nicht weiter tragisch, da der Test im Zweifel lieber zu viel als zu wenig bemängeln soll.

def safe_associations(target_class)
  associations =
    target_class.reflect_on_all_associations(:has_many) +
    target_class.reflect_on_all_associations(:has_one)
  associations.select{ |assoc| assoc.options[:dependent].in?(%i{destroy nullify}) && assoc.scope.nil? }
end

Shared Example für verdächtige Keys

Nun können die gültigen Konfigurationen von den Fremdschlüsseln abziehen. Nur wenn die Differenz leer ist, sind alle Schlüssel konfiguriert. Da wir bei Scopes etwas übervorsichtig sind, erlauben wir aber auch eine ignore-Option, um bestimmte Fremdschlüssel zu ignorieren:

shared_examples 'a dependent model' do  |options = {}|
  it 'has a dependent option for has_* associations' do
    foreign_keys = foreign_keys(described_class.table_name)
    associations = safe_associations(described_class)
    ignore = options[:ignore] || []

    suspicious_keys = 
      foreign_keys.map{ |key| "#{key.from_table}.#{key.column}" } -
      associations.map{ |assoc| "#{assoc.klass.table_name}.#{assoc.foreign_key}" } -
      ignore

    expect(suspicious_keys).to be_empty
  end

  # ...
end

Nun noch die Fehlermeldungen modifizieren und etwas Doku ergänzen und schon kann das Example in Model-Specs eingebunden werden:

describe BookWithChapters do
  it_behaves_like 'a dependent model'
end

Verwischte Font-Awesome-Icons in Phantom JS

Wir reichern unsere auf Capybara und Phantom JS basierenden Feature-Tests gerne mit Screenshots an. Hierbei kam es regelmäßig zu einem Problem mit Font Awesome, wodurch der automatische Abgleich fehl schlug. Bei einzelnen Specs tritt das Problem nicht auf, erst wenn mehrere Tests mit unterschiedlichen Seiten abgelichtet werden zeigte sich folgendes Phänomen:

font-awesome-phantomjs

Alle Icons, wie hier das Such-Icon rechts, wurden wie links zu sehen verzerrt. Den Lösungsansatz brachte dieser Eintrag auf Stackoverflow: Ist die Schriftart nativ auf dem System vorhanden, tritt die Verzerrung nicht mehr auf. Unter Ubuntu ist das schnell gemacht:

sudo apt-get install fonts-font-awesome
sudo fc-cache -f -v

Herausfinden, wo eine Methode definiert wurde

In manchen Situationen ist es alles andere als offensichtlich, welche Klasse oder welches Modul eine Methode bereitstellt. Wer implementiert die Get-Methode für Rspec-Controller-Tests? Gerade in solch zusammengewürfelten Umgebungen ist das nur schwer nachvollziehbar. Ruby hilft hierbei: Die Metamethode method gibt Auskunft, woher eine Methodendefinition stammt:

require "rails_helper"

RSpec.describe MyController do
  it do
    puts method(:get)
  end
end

# => #<Method: RSpec::ExampleGroups::MyController(ActionController::TestCase::Behavior)#get>

Das Beispiel zeigt nicht nur die Klasse, sondern auch das includete Modul, welches für die Methode verantwortlich ist. Dies hilft beim Googeln oder man verwendet Ri:

ri ActionController::TestCase::Behavior#get

 

Match API output with YAML fixture

A nice and simple approach to test you JSON APIs:

# spec/features/api/user_spec.rb

require 'rails_helper'

RSpec.feature 'User', type: :feature do
  it 'lists users' do
    user = create(:user, :as_admin)
    visit api_user_path(user, format: :json)
    expect(page).to match_yaml_fixture('api/user')
  end
end

This checks if the page source matches the data given in the fixture file. To make it more readable we format the expected JSON as YAML:

# spec/fixtures/api/user.yaml

---
user:
- id: 1
  first_name: Max
  last_name: Power
  role:
    id: 1
    name: Admin

weiterlesen

ActiveAdmin: Authentifizierung für Browser und API

Eine einfache API-Authentifizierung lässt sich in ActiveAdmin durch Wiederverwendung des Standard-Admin-Users im Initializer erreichen:

 # config/initializers/active_admin.rb
 ActiveAdmin.setup do |config|
   config.prepend_before_filter do
     if active_admin_config.namespace.name == :api
       authenticate_or_request_with_http_basic('API') do |name, password|
         user = AdminUser.find_by_email!(name)
         sign_in(:admin_user, user) if user.valid_password?(password)
       end
     end
   end

Dies erlaubt die Angabe des Benutzernamens und Passworts im Browser als Popup (HTTP-Basic-Authentication) und als Header für APIs:

curl -u admin@example.com:password http://localhost:3000/api/my_resources.json

weiterlesen

Bundler is using a binstub that was created for a different gem?

In regelmäßigen Abständen wirft unsere Konfiguration aus Bundler 1.7, Rails 4.1, Rspec 2.14 und Spring 1.1 bei jedem Rails-, Rake- oder Rspec-Befehl die folgende Warnung:

Bundler is using a binstub that was created for a different gem.
This is deprecated, in future versions you may need to `bundle binstub rspec` to work around a system/bundle conflict.

Auch andere Nutzer scheinen davon verwirrt zu sein, eine klare Erklärung des Problems und seiner Lösung konnte ich bisher nicht finden. Folgender Reset der Binstubs funktioniert für mich als Workaround:

rm -rf bin/*
bundle exec spring binstub --all
bundle binstub rspec-core --force

Spring ohne Bin-Präfix nutzen

Spring lässt sich sehr einfach installieren: Gem eintragen und die Binstubs erzeugen:

bundle exec spring binstub --all

Um nun rake routes oder rails generate statt bin/rake routes oder bin/rails generate nutzen zu können, einfach die folgenden Funktionen in beispielsweise die bash_aliases eintragen:

rake() { if [ -f bin/rake ]; then bin/rake "$@"; else bundle exec rake "$@"; fi }
rails() { if [ -f bin/rails ]; then bin/rails "$@"; else bundle exec rails "$@"; fi }
rspec() { if [ -f bin/rspec ]; then bin/rspec "$@"; else bundle exec rspec "$@"; fi }

Mein Dankeschön für den Ansatz geht an Arne.

weiterlesen

RSpec durch Parallelisierung beschleunigen

Mit der testgetriebenen Entwicklung gibt es vor allem ein großes Problem:

Finished in 2 minutes 32.8 seconds
547 examples, 0 failures, 4 pending

Schon in einem frühen Stadium läuft die Testsuite locker zwei Minuten. Parallel Tests will die Dauer verkürzen, indem es Tests auf mehreren Prozessorkernen gleichzeitig ausführt.

Ein wesentlicher Aspekt bei der Parallelisierung ist der gleichzeitige Zugriff unterschiedlicher Prozesse auf eine Datenbank. Das Gem behebt die auftretenden Konflikte, indem es jedem Prozess seine eigene Datenbank zuweist. Die Anzahl der Prozesse richtet sich dabei nach den verfügbaren Kernen. weiterlesen

RSpec und PostgreSQL: Insufficient Privilege

Bei einem rake spec erhielt ich den folgenden Fehler:

PG::InsufficientPrivilege: ERROR:  permission denied to create database
[...]

Es mag eine falsche Einstellung sein (ein einfaches rspec verursacht das Problem nicht), aber offensichtlich benötigt der Datenbanknutzer in meiner Konfiguration das Recht, neue Datenbanken zu erstellen. Auf meiner lokalen Maschine, wo die Tests laufen, kann ich das verantworten.

Postgres-Kommandozeile aufrufen:

sudo su - postgres
psql

Recht zum Erstellen der Datenbank an Nutzer vergeben:

ALTER USER "my-db-user" CREATEDB;