Testbarkeit (in Ruby)

60134178a94193fb4b8a28cdb4771440?s=47 elitau
August 23, 2014

Testbarkeit (in Ruby)

Tests steigern die Qualität und das Vertrauen in die Software. Wie muss nun Code gestaltet werden, so dass die Tests dazu einfacher geschrieben werden können? Was sind die typischen Schwachstellen von Ruby Programmen? Und wie schreibt man Code, der es leicht macht, diese Schwachstellen durch Tests offen zu legen? In diesem Vortrag gehe ich anhand von Beispielen auf die Attribute von schwer testbarem und fehleranfälligem Ruby Code ein und stelle Patterns und Refactorings vor, um die Tests zu vereinfachen.

60134178a94193fb4b8a28cdb4771440?s=128

elitau

August 23, 2014
Tweet

Transcript

  1. TESTBARKEIT IN RUBY Eduard Litau @hurx

  2. INHALT • Qualität • Testbarkeit • Schwachstellen • Patterns und

    Refactorings
  3. QUALITÄTSMODELL

  4. QUALITÄTEN & TESTS Unit + Integration Tests external quality quality

    in use internal quality process quality influences influences influences depends on Acceptance Tests User Tests depends on depends on Retrospective Source: ISO/IEC SQuaRE 25010
  5. TEST-FEEDBACK Test Level Unit Amount of feedback Integration Acceptance

  6. TESTEN VON SOFTWARE “Program testing can be used to show

    the presence of bugs, but never to show their absence!”
 Edsger W. Dijkstra • Reviews, Code-Metriken • Überblick über große Systeme • Potentielle Designprobleme • Einfache Anwendung • Schwierige Interpretation • Ausführen des Testobjekts • Black- und Whitebox Verfahren • Verifiziert Spezifikationen • Deckt Fehler auf • Einsatz aufwändig • Einfache Interpretation Dynamisches Testen Statisches Testen
  7. VORTEILE DURCH TESTBARKEIT • Reduziert Aufwand für Tests, damit auch

    Kosten und • Erleichtert das frühe Auffinden von Fehlern • Testbare Software erfüllt meist auch etablierte Gestaltungsprinzipien für OOP • Diagnose von Fehlern wird erleichtert
  8. TESTBARKEIT • Beobachtbarkeit • Kontrollierbarkeit • Softwaredesign „The ease with

    which test criteria can be established for a system or component and tests can be performed to determine whether those criteria have been met.“ [ISO 25010]
  9. KONTROLLIERBARKEIT • Wie leicht kann das Testobjekt ausgeführt werden? •

    Hindernisse: • Abhängigkeiten • Komplexer Code Testobject Input Output System (-zustand)
  10. BEOBACHTBARKEIT • Wie ist der Zustand des Systems nach einem

    Test? • (Test-)Schnittstellen • Notwendig für Automatisierung Testobject Input Output System (-zustand)
  11. SOFTWAREDESIGN • Geringe Komplexität • Zusammenhängender (kohäsiver) Code • Isolierbare

    Komponenten • Lose gekoppelter Code • Keine Redundanzen
  12. FEHLERANALYSE • Analyse der Bugreports und Exceptions von simfy und

    Spree • Kategorisierung von Fehlern und Extraktion von Schwachstellen
  13. FEHLERVERTEILUNG 3 % 3 % 9 % 15 % 21

    % 21 % 29 % Fehlerhafte Objektreferenzen Methode nicht vorhanden Vertragsverletzung Falsche oder fehlende Parameter Methode hat geringe Kohäsion Inkonsistente Komponente Inkorrekte Typ-Überführung
  14. SCHWACHSTELLEN • nil • Options-Hash • Module • Metaprogramming

  15. NIL • NoMethodError: undefined method `foo' for nil:NilClass • Viele

    Quellen für nil: Hash#[], Enumerable#map, etc. • [GOOS]: „Never Pass Null between Objects“ • Tell, Don’t Ask
  16. BEISPIEL def greeting_for_user(user) if user && user.name "Hello #{user.name}" end

    end
  17. BEISPIEL class TeaserRepository ! def initialize(user, params) @current_user = user

    @params = params end ! def locale @params[:locale].presence || @current_user.locale end end
  18. DESIGN BY CONTRACT • Entwurfskonzept nach Bertrand Meyer • Vor-,

    Nachbedingungen & Invarianten • Ausführbare Kommentare & Tests in Einem • Fail Early
  19. DESIGN BY CONTRACT class TeaserRepository include Assertions def initialize(user, params)

    assert { params || current_user } @current_user = user @params = params end ! def locale @params[:locale].presence || @current_user.locale end end module Assertions class AssertionFailedError < StandardError; end def assert(&condition, message = "Assertion failed") raise AssertionFailedError.new(message) unless condition.call end end
  20. NULL-OBJECT PATTERN • „Use Special Case / Nullobject whenever you

    have multiple places in the system that have the same behavior after a conditional check for a particular class instance, or the same behavior after a null check.“ [Fowler, 2002, S. 497] • Tell, Don't Ask • Ersetzt nil-Objekte durch explizit benannte Logik • Fördert Kapselung
  21. NULL OBJECT BEISPIEL def greeting_for_user(user) if user && user.name "Hello

    #{user.name}" end end class NotLoggedInUser def name(name) "Gast" end end def greeting_for_user(user) if user.name "Hello #{user.name}" end end
  22. MODIFIER-/QUERY-METHOD • Methoden haben entweder beobachtbare Nebeneffekte oder geben einen

    Wert zurück • Reduziert Verantwortlichkeit von Methoden und damit Kontrollierbarkeit • Query - Methoden steigern Beobachtbarkeit
  23. MODIFIER-/QUERY BEISPIEL class Subscription def check_status_and_send_mail if invoice.paid? self.status =

    "paid" MailService.send_paid_subscription(self) else self.status = "pending" end return status end end describe Subscription do let(:subscription) { Subscription.new } it "should return status 'paid' and send mail if invoice paid" do allow(subscription.invoice).to receive(:paid?).and_return(true) expect(MailService).to receive(:send_paid_subscription) expect(subscription.check_status_and_send_mail).to eql('paid') end it "should return status 'pending' and if invoice not paid" do allow(subscription.invoice).to receive(:paid?).and_return(false) expect(subscription.check_status_and_send_mail).to eql('pending') end end
  24. MODIFIER-/QUERY BEISPIEL class Subscription def status invoice.paid? ? 'paid' :

    'pennding' end def send_mail_if_paid MailService.send_paid_subscription(self) if status == 'paid' end end describe Subscription do let(:subscription) { Subscription.new } it "should return status 'paid' if invoice is paid" do allow(subscription.invoice).to receive(:paid?).and_return(true) expect(subscription.status).to eql('paid') end ! it "should return status 'pending' if invoice is not paid" do allow(subscription.invoice).to receive(:paid?).and_return(false) expect(subscription.status).to eql('pending') end it "should send mail if status is 'paid'" do subscription = Subscription.new(status: 'paid') expect(MailService).to receive(:send_paid_subscription) subscription.send_mail_if_paid end end
  25. BEDINGTE AUSFÜHRUNG DURCH BLÖCKE • Entscheidung zur Ausführung von Code

    an die aufgerufene Methode verlagern • Reduziert nil Checks • Hilft bei Trennung von Methoden mit und ohne Nebeneffekt
  26. BLÖCKE BEISPIEL class UserStore def find_by_name(name, &block) if user =

    Database.find_record(:type => :user, :name => name) block.call(user) end end end class UsersController def update UserStore.find_by_name(params[:username]) do |user| user.updated_at = Time.now # ... end end end
  27. WEITERE REFACTORINGS • Extract Method, Class und Module - Refactorings

    steigern Kohäsion • Immutability • In-Out Funktionen ohne Seiteneffekte
  28. WAS TESTEN? • Neben dem reinen Outside-In Development sollten Edgecases

    nicht vergessen werden, v.a. auf/mit nil testen oder besser nil verhindern
  29. FAZIT • Wahrgenommene Qualität hängt von der inneren Qualität und

    dem Entwicklungsprozess ab • Testbare Software folgt OOP Designprinzipien • nil vermeiden hilft beim Fehler reduzieren
  30. QUELLEN • [GOOS]: Growing Object-Oriented Software, Guided by Tests -

    Steve Freeman and Nat Pryce • ISO/IEC 25010 SQuaRE (successor to ISO 9126) • [Fowler 2002] Patterns of Enterprise Application Architecture - Martin Fowler • [Binder]: Testing Object-Oriented Systems: Models, Patterns, and Tools - Robert Binder