RSpec – Sebastians Blog https://sgaul.de Neues aus den Softwareminen Mon, 04 Mar 2019 13:01:24 +0000 de-DE hourly 1 https://wordpress.org/?v=6.1.1 https://sgaul.de/wp-content/uploads/2019/02/cropped-sgaul-2-1-32x32.jpg RSpec – Sebastians Blog https://sgaul.de 32 32 Vollständige Dependent-Einstellungen in Rails-Models testen https://sgaul.de/2019/03/04/vollstaendige-dependent-einstellungen-in-rails-models-testen/ Mon, 04 Mar 2019 15:38:13 +0000 https://sgaul.de/?p=2933 Vollständige Dependent-Einstellungen in Rails-Models testen weiterlesen]]> 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 https://sgaul.de/2016/07/07/verwischte-font-awesome-icons-in-phantom-js/ Thu, 07 Jul 2016 15:48:15 +0000 https://sgaul.de/?p=2874 Verwischte Font-Awesome-Icons in Phantom JS weiterlesen]]> 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 https://sgaul.de/2016/04/26/herausfinden-wo-eine-methode-definiert-wurde/ Tue, 26 Apr 2016 10:37:57 +0000 https://sgaul.de/?p=2870 Herausfinden, wo eine Methode definiert wurde weiterlesen]]> 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 https://sgaul.de/2015/10/12/match-api-output-with-yaml-fixture/ Mon, 12 Oct 2015 13:23:50 +0000 https://sgaul.de/?p=2834 Match API output with YAML fixture weiterlesen]]> 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

Add match_yaml_fixture to RSpec

Drop this in your support directory:

require 'rspec/expectations'

module FixtureHelper
  def read_fixture(*path_elements)
    IO.read(Rails.root.join("spec", "fixtures", *path_elements))
  end
end

RSpec::Matchers.define :match_yaml_fixture do |expected|
  match do |actual|
    expected = prepare_expected(expected)
    actual = prepare_actual(actual)
    actual.strip == read_fixture(expected).strip
  end
  failure_message do |actual|
    expected = prepare_expected(expected)
    actual = prepare_actual(actual)
    "expected the following to match fixture file #{expected}:\n\n#{actual}"
  end

  private

  def prepare_expected(expected)
    expected += '.yaml' unless expected.end_with?('.yaml')
    expected
  end

  def prepare_actual(actual)
    actual = JSON.parse(actual.source) if actual.is_a?(Capybara::Session)
    actual = actual.to_yaml if actual.is_a?(Hash)
    actual.to_s
  end
end

Then include the helper in your spec_helper.rb or rails_helper.rb file:

# spec/rails_helper.rb

require File.expand_path('../support/fixture_helper', __FILE__)
RSpec.configure do |config|
  config.include FixtureHelper
end
]]>
ActiveAdmin: Authentifizierung für Browser und API https://sgaul.de/2015/09/22/activeadmin-authentifizierung-fuer-browser-und-api/ Tue, 22 Sep 2015 14:37:09 +0000 https://sgaul.de/?p=2805 ActiveAdmin: Authentifizierung für Browser und API weiterlesen]]> 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

Will man dies mit Capybara ohne JS testen, hat sich für mich der folgende Ansatz bewährt:

page.driver.browser.basic_authorize(user.email, user.password)
visit api_my_resources_path

Bei anderen Treibern (zum Beispiel bei Verwendung von Javascript) kann die Schnittstelle abweichen.

]]>
Bundler is using a binstub that was created for a different gem? https://sgaul.de/2014/12/04/bundler-is-using-a-binstub-that-was-created-for-a-different-gem/ Thu, 04 Dec 2014 14:45:23 +0000 https://sgaul.de/?p=2718 Bundler is using a binstub that was created for a different gem? weiterlesen]]> 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 https://sgaul.de/2014/11/18/spring-ohne-bin-praefix-nutzen/ Tue, 18 Nov 2014 15:05:00 +0000 https://sgaul.de/?p=2707 Spring ohne Bin-Präfix nutzen weiterlesen]]> 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.

Um die ursprünglichen Varianten ohne Spring zu nutzen kann command verwendet werden:

command rake routes
]]>
RSpec durch Parallelisierung beschleunigen https://sgaul.de/2014/01/04/rspec-durch-parallelisierung-beschleunigen/ Sat, 04 Jan 2014 16:00:41 +0000 https://sgaul.de/?p=2434 RSpec durch Parallelisierung beschleunigen weiterlesen]]> 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.

Einrichtung

Gemfile:

group :development do
  # ...
  gem 'parallel_tests'
end

config/database.yml:

test: &test
  database: myapp-test<%= ENV['TEST_ENV_NUMBER'] %>
bundle
rake parallel:create

Der Task beschwert sich, das eine der Datenbanken bereits existiert. Dies ist jedoch nur ein Hinweis, die Duplikate werden wie gewünscht erzeugt.

Schemastand anpassen

Nach der Einrichtung und nach jeder Migration müssen die Schemata der Datenbanken angeglichen werden. Auch hierfür liefert das Gem den passenden Task:

rake parallel:prepare

Mit vier Kernen die Zeit halbieren

Das Ergebnis ist in meinem Beispielfall recht beeindruckend. Die parallelisierte Variante benötigt nicht einmal die Hälfte der Zeit:

rake parallel:spec
547 examples, 0 failures, 4 pendings
Took 67.945901274 seconds

Problematisch ist in dieser Konfiguration die Ausgabe: Alle Prozesse schreiben gleichzeitig ins Terminal, was jede RSpec-Visualisierung zerschießt. Die auf der Github-Seite vorgeschlagenen Logger brachten entweder keine Besserung oder führten dazu, dass Fehlermeldungen gar nicht mehr angezeigt wurden.

Zeus- und Guard-Unterstützung

Wer seine Tests nicht immer selbst auslösen will und auf die zehn Sekunden Wartezeit für die Rails-Umgebung verzichten kann, sollte sich dieses Gem ansehen, welches die drei Komponenten Guard, Zeus und Parallel Tests zusammenschraubt.

]]>
Rspec: Arrayvergleich ohne Reihenfolge https://sgaul.de/2013/10/23/rspec-arrayvergleich-ohne-reihenfolge/ Wed, 23 Oct 2013 17:13:28 +0000 https://sgaul.de/?p=2412 Um zwei Arrays auf Gleichheit der Elemente zu testen, ohne dabei die Reihenfolge zu beachten, kann der Rspec-Matcher match_array verwendet werden:

expect([1, 2, 3]).to match_array([1, 2, 3]) 
expect([1, 2, 3]).to match_array([3, 2, 1]) 
expect([1, 2, 3]).to match_array([1, 2, 4]) 
]]>
RSpec und PostgreSQL: Insufficient Privilege https://sgaul.de/2013/09/17/rspec-und-postgresql-insufficient-privilege/ Tue, 17 Sep 2013 15:01:43 +0000 https://sgaul.de/?p=2352 RSpec und PostgreSQL: Insufficient Privilege weiterlesen]]> 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;
]]>