Aus Linux-Magazin 04/2005

Ruby als automatischer Qualitätssicherungs-Assistent

Bei jeder Änderung an einem Programm besteht die Gefahr, dass unbemerkt neue Fehler entstehen. Mit Unit-Tests kontrollieren Entwickler, ob alle Bausteine ihres Programms erwartungsgemäß funktionieren. Die Skriptsprache Ruby bringt von Haus aus ein leistungsfähiges Modul fürs Unit Testing mit.

Welcher Programmierer hätte nicht gern einen persönlichen Assistenten, der nach Änderungen kontrolliert, ob alles so funktioniert, wie es soll? Software testen kostet viel Zeit, ebenso die Fehlersuche. Um keine neuen Fehler in ein funktionierendes System einzubauen, scheuen viele Programmierer wichtige strukturelle Änderungen.

Die Antwort auf diese Probleme sind Unit-Tests. Sie sollen kontrollieren, ob ein kleines Stück Code genau das tut, was der Entwickler möchte. Der Test selbst ist ein Programm, das den zu testenden Code ausführt, Rückgabewerte vergleicht und etwaige Nebeneffekte aufdeckt. Der Kasten “Warum testen?” liefert weitere Argumente fürs richtige Testen von Software.

Unit-Tests werden oft mit anderen Formen von Software-Tests verwechselt. Ein Unit-Test hat nichts mit einem funktionalen Test zu tun, bei dem das Verhalten einer Software an den äußeren Schnittstellen (wie dem Userinterface) untersucht wird. Es geht beim Unit Testing darum, sich zu vergewissern, dass die einzelnen Bestandteile des Code nach Wunsch funktionieren. Aus diesem Grund wird Unit Testing oft auch als Developer Testing bezeichnet.

Tests planen

Wenn der Programmierer damit beginnt, Unit-Tests zu schreiben, ist eine seiner wichtigsten Fragen, wie viele und welche Tests nötig sind. Natürlich kann er jede einzelne Methode ausführlich untersuchen und damit einen Großteil der potenziellen Fehler vermeiden. Der notwendige Aufwand steht aber oft nicht mehr im Verhältnis zum Nutzen und schreckt daher viele davon ab, überhaupt zu testen.

Es geht beim Testen also zuerst um jene Funktionen, die das höchste Fehlerrisiko aufweisen. Besonders wichtig ist es, Ausnahmebedingungen, Sonderfälle und Grenzbereiche zu überprüfen, da sich hier am häufigsten Fehler verstecken.

Im Lieferumfang enthalten

Das Unit Testing Framework »Test::Unit« ist seit Ruby 1.8 Bestandteil der Standarddistribution. Das früher verwendete Modul »runit« existiert weiterhin als Wrapper um »Test::Unit«, der Rückwärtskompatibilität gewährleistet.

require 'test/unit'
class TestFactorial < Test::Unit::TestCase
   def test_calc
   end
end

Dieser Code soll ein Programm untersuchen, das die Fakultät einer Zahl berechnet. Ausgangspunkt für den Testprogrammierer ist zunächst das Testmodul. Die Testklasse leitet sich von »Test::Unit ::TestCase« ab. Alle Methoden, die mit »test« beginnen, arbeitet das Test-Framework automatisch ab, sobald der Ruby-Interpreter die Datei ausführt.

Jeder erfolgreiche Test wird durch einen Punkt dargestellt (Abbildung 2). Ein »F« steht für Failure (Versagen) und signalisiert, dass der Test nicht die erwarteten Rückgabewerte liefert. Das »E« ist die Kurzform für Error (Fehler) und bedeutet, dass innerhalb des Tests ein Programmfehler aufgetreten ist, beispielsweise eine Ausnahme. Die letzte Zeile informiert über die Anzahl der Tests, der Assertions und der aufgetretenen Fehler.

Assertions

Um ein Programm testen zu können, bietet »Test::Unit« eine Vielzahl so genannter Assertions (Behauptungen) an. Das sind einfache Methoden, die überprüfen, ob eine Annahme wahr ist. Wird eine der Annahmen nicht erfüllt, gibt das Test-Framework einen Fehler aus. Wichtige Assertions sind:

  • »assert(boolean)«
  • »assert_equal(expected, actual)«
  • »assert_nil(object)«
  • »assert_raise(*args) { block }«
Abbildung 1: Schematischer Aufbau eines Unit-Tests. Die Testsuite beinhaltet eine weitere Sammlung Testsuite 1 und einen Testfall Testcase.

Abbildung 1: Schematischer Aufbau eines Unit-Tests. Die Testsuite beinhaltet eine weitere Sammlung Testsuite 1 und einen Testfall Testcase.

Abbildung 2: Der Konsolen-Testrunner gibt seine Ergebnisse im Textformat aus und dokumentiert dabei jeden erfolgreichen Test mit einem Punkt.

Abbildung 2: Der Konsolen-Testrunner gibt seine Ergebnisse im Textformat aus und dokumentiert dabei jeden erfolgreichen Test mit einem Punkt.

Die Methode »assert_raise« erwartet als Argumente die Ausnahmen (Exceptions), die der Block bei der Ausführung auslöst. Allen Assertions lässt sich als letztes Argument ein erklärender Text übergeben, der im Fehlerfall angezeigt wird, zum Beispiel »assert(nil, “der Wert ist nicht richtig”)«.

Zu fast allen Assertions gibt es auch das Gegenteil der Bedingung, zu »assert_equal« also beispielsweise »assert_not_equal«. Natürlich sind die meisten Assertions, die spezielle Bedingungen überprüfen, auch durch ein einfaches »assert« ersetzbar. Als Parameter dient in diesem Fall der entsprechende Vergleich, also zum Beispiel »assert(1==1)« statt »assert_equal(1,1)«. Allerdings fördern die spezialisierten Assertions die Lesbarkeit sowohl des Code als auch der Testergebnisse. Eine komplette Auflistung aller Assertions findet sich in der API-Dokumentation von »Test::Unit« unter[1].

Mit diesen Assertions kann der Testcode das Fakultätsprogramm überprüfen. Wichtig ist dabei, immer auf Sonderfälle hin zu untersuchen, in diesem Fall die Eingabe von negativen Werte, Fließkommazahlen oder Text. Die Anweisung »assert_nil(f.calc(-1))« beispielsweise überprüft, ob das Programm für negative Eingabewerte richtigerweise kein Ergebnis zurückgibt. Den gesamten Beispielcode zeigt Listing 1.

Setup, Teardown und Fixtures

Damit ein Test automatisiert ablaufen kann und reproduzierbare Ergebnisse liefert, sollte er nicht auf echten veränderlichen, sondern auf fixen Datensätzen beruhen. Solche unveränderlichen Daten, aber auch damit initialisierte Objekte heißen Fixtures. Der Programmierer definiert sie direkt in der Testdatei oder in separaten Files. Wichtig ist nur, dass die Datenquelle verfügbar und schnell genug ist, damit das Einlesen der Daten die Tests nicht beeinträchtigt. E

Abbildung 3: Der GTK-2-Testrunner zeigt einen Fehler an und gibt detaillierte Informationen aus: Das getestete Programm hat eine ungültige URL verwendet.

Abbildung 3: Der GTK-2-Testrunner zeigt einen Fehler an und gibt detaillierte Informationen aus: Das getestete Programm hat eine ungültige URL verwendet.

Ruby führt vor jedem Test die Methode »setup« aus, die sich daher für die Vorbereitung häufig verwendeter Fixtures anbietet. Falls nötig hilft die Methode »teardown« benutzte Testdatenquellen und Ressourcen wieder sauber zu schließen.

Tests ablaufen lassen

Die so genannten Testrunner führen die Tests aus und bereiten zudem die Ergebnisse übersichtlich auf. Standardmäßig verwendet »Test::Unit« den Testrunner der Kommandozeile (siehe Abbildung 2).

Für eine grafische Ausgabe ist einfach der Testrunner für das gewünschte Userinterface zu wählen, zum Beispiel der TK-Testrunner für eine Darstellung mit TK beziehungs-weise der GTK-2-Testrunner für die Anzeige mit dem GTK-2-Toolkit (siehe Abbildung 3). Einen solchen grafischen Testrunner kann natürlich nur verwenden, wer die Ruby-Module für das entsprechende Toolkit installiert hat.

Der folgende Code bindet erst den Testrunner ein und führt danach dessen »run«-Methode aus:

require 'test/unit/ui/gtk2/testrunner'
Test::Unit::UI::GTK2::TestRunner.run(Test)

Der übergebene Parameter (»Test«) darf ein einzelner Test oder eine ganze Testsuite sein. Es ist wichtig, den Testrunner nur auf der obersten Ebene anzugeben, da die einzelnen Tests sonst mehrfach durchlaufen.

Testsuites fassen mehrere Unit-Tests sowie bereits bestehende Testsuites zu einem Paket zusammen. Damit bleibt auch in einem größeren Projekt der Überblick über alle verwendeten Tests erhalten. Die einfachste Methode zur Erstellung einer Testsuite ist, in einer eigenen Datei alle benötigten Tests und das »Test::Unit«-Framework einzubinden.

require 'test/unit'
require 'test_case_1'
require 'test_suite_1'

Das Diagramm in Abbildung 1 veranschaulicht, wie eine solche Sammlung von Tests zusammenhängt.

Listing 1:
Fakultätsprogramm mit Test

01 # test_factorial.rb
02 require 'test/unit'
03 require 'factorial'
04 class TestFactorial < Test::Unit::TestCase
05    def test_calc
06       f = Factorial.new
07       assert_equal(120, f.calc(5) )
08       assert_equal(1, f.calc(1) )
09       assert_equal(1, f.calc(0) )
10       assert_nil(f.calc(1.3) )
11       assert_nil(f.calc(-1) )
12       assert_nil(f.calc("text"))
13    end
14 end
15 
16 # factorial.rb
17 class Factorial
18    def calc number
19       return nil if !number.is_a?(Integer) || number < 0
20       @result = 1
21       number.downto(1) { |i|
22          @result *= i
23       }
24       return @result
25    end
26 end

Listing 2:
»article.rb«

01 require 'net/http'
02 
03 class Article
04    def initialize
05       @host = 'www.linux-magazin.de'
06       @baseurl = '/Artikel/ausgabe/'
07    end
08 
09    def parse_articles(text)
10       ...
11    end
12 
13    def load_article_page(year, month)
14       ...
15    end
16 
17    def read_articles(year, month)
18       ...
19    end
20 
21    private
22    def createConnection
23       ...
24    end
25 
26    def get_address(year, month, suffix=nil)
27       ...
28    end
29 
30    def extract_article_text(text)
31       ...
32    end
33 
34    def extract_article_link(text, base)
35       ...
36    end
37 end
38 
39 ...

Zugriff auf private Methoden durch Vererbung

Das Testen der öffentlichen Methoden einer Klasse reicht in den meisten Fällen aus, denn es stellt dabei die korrekte Funktion der privaten Methoden sicher. Es gibt aber dennoch einen Weg, eine als »private« oder »protected« markierte Methode direkt zu testen: Man leitet von der zu testenden Klasse eine Subklasse ab, die geschützte Methoden als öffentlich definiert. Im folgenden Beispiel besitzt die »Klasse« eine geschützte (»protected«) Methode »geheim«. Die Unterklasse »Subklasse« besitzt eine gleichnamige öffentliche Methode, die mit »super« die geschützte Methode der Elternklasse aufruft:

class Klasse
   protected
   def geheim; end
end

class Subklasse < Klasse
   def geheim; super; end
end

Eine noch einfachere Möglichkeit bietet die Methode »send«, die auch geschützte Methoden aufrufen kann. Das Beispiel im obigen Code ist auch mit » Instanz .send :geheim« umsetzbar. Diese Fähigkeit der »send«-Methode ist zwar nicht dokumentiert, wird aber auch in Beispielen der Ruby-Dokumentation für ähnliche Zwecke verwendet. Optional lässt sich die Testsuite danach explizit als Klasse anlegen, bei der die Klassenmethode »suite« alle gewünschten Tests einbindet. Diese Klasse bekommt der Testrunner übergeben:

class Suite
   def self.suite
      suite = Test::Unit::TestSuite.new
      suite << TestCase1.suite
      suite << TestSuite1.suite
      return suite
   end
end
require 'test/unit/ui/gtk2/testrunner'
Test::Unit::UI::GTK2::TestRunner.run(Suite)

Listing 3 zeigt Beispiele zum Testen privater Methoden: So testet »test_get_address« die private Methode »get_address« direkt. Die erste Assertion der Testmethode »test_read_articles« überprüft dagegen, ob im weiteren Verlauf das Programm die richtige Adresse verwendet. Damit prüft sie indirekt gleichzeitig die korrekte Funktion der »get_address«-Methode.

Warum testen?

Aus welchen Gründen sollte ein Programmierer zusätzlich Zeit aufwenden, um für Tests ein zweites Programm ohne eigene Funktionalität parallel zum eigentlichen Programm zu schreiben?

Absicherung bei Änderungen am Programm

Programmierer verbringen oft einen überraschend kleinen Teil ihrer Zeit mit dem Schreiben des Programms, einen deutlich größeren mit der Fehlerbehebung. Dabei sind die meisten Fehler schnell ausgebessert – den Fehler finden kostet die meiste Zeit. Zu allem Überfluss können durch jede Änderung neue Fehler entstehen, gegen die sich Entwickler mit guten Unit-Tests absichern können.

Bei regelmäßiger Ausführung der Tests ist der Fehler sehr schnell gefunden, weil nur der Code, der seit dem letzten Testdurchlauf hinzugekommen ist, überhaupt zur Debatte steht. Das ist besonders für das Refactoring wichtig, also für Änderungen am Programm, um dessen Struktur (nicht dessen Funktionalität) zu verbessern.

Unit-Tests sind zwar kein Allheilmittel gegen Programmfehler, können aber dabei helfen, deutlich weniger Arbeitszeit mit der Fehlerbehebung zu verbringen.

Ausnahmesituationen und Grenzfälle

Unit-Tests eignen sich dazu, alle Arten von Ausnahmesituationen und Grenzfällen zu kontrollieren. In diesen Bereichen verstecken sich sehr häufig Fehler, die in einem nicht getesteten Programm selten auftreten und dadurch schwierig zu eliminieren sind.

Dokumentation des Programms

Unit-Tests dokumentieren ein Programm, indem sie mit dem Interface der getesteten Klasse kommunizieren und die erwarteten Rückgabewerte festlegen.

Test Driven Development

Beim Test Driven Development (TDD) werden die Tests vor der Implementation des Programms erstellt. Das Programm gilt als fertig gestellt, sobald alle Tests ohne Fehler absolviert werden. Beim Schreiben eines Tests muss sich der Programmierer Gedanken über die Funktionsweise einer Klasse machen. Der Fokus liegt dabei automatisch auf dem Interface und nicht auf der Implementation – eine der wichtigsten Richtlinien für objektorientiertes Programmieren. Das führt meist zu übersichtlicherem Code. Entwickler, die zuerst die Tests schreiben, versetzen sich in den Klassenanwender und werden das Interface (hoffentlich) verständlicher aufbauen.

Dokumentation der Fehler

Tritt ein Fehler auf, der nicht durch einen Test abgedeckt ist, schreibt der Entwickler einen Test, der diesen Fehler dokumentiert. Sobald der Test fehlerfrei durchgeführt wird, ist der Bug behoben. So ist auch sichergestellt, dass derselbe Fehler kein zweites Mal unbemerkt durch die Tests rutscht.

Nachmachen hilft

Ein Test sollte möglichst wenig von anderen Bestandteilen des Systems abhängen, damit die Testsoftware eventuell auftretende Fehler auch an der richtigen Stelle meldet. In der Praxis bestehen aber in jedem System Abhängigkeiten, die das Testen schwieriger machen. Problematisch sind vor allem nicht immer verfügbare Ressourcen oder Funktionen, deren Aufruf unerwünschte Nebeneffekte hat. Je nach Art der Ressource muss der Programmierer hier andere Vorkehrungen zu treffen.

So genannte Mock-Objekte (Nachahmung) bieten die Möglichkeit, ein zu testendes Objekt von solchen Abhängigkeiten und Unwägbarkeiten zu isolieren, indem sie das Interface der benötigten Klasse nachahmen. Ein Mock ist also eine einfache Ersatzimplementation einer vom Produktionscode benötigten Klasse. Der Test parametrisiert ein Mock-Objekt mit den gewünschten Ein- und Ausgabewerten. Damit überprüft er, ob der getestete Code die richtigen Werte an das Objekt übergibt oder mit verschiedenen Rückgabewerten zurechtkommt. Mock-Objekte können auch ein praktischer Bestandteil des Test Driven Development sein, wenn man aus ihnen das Interface für den eigentlichen Produktionscode ableitet.

Selbstverständlich ist es keine Option, nur für den Test den Produktionscode zu verändern, damit statt des eigentlichen Objekts das Mock-Objekt aufgerufen wird. Schließlich sollen Unit-Tests automatisiert ablaufen. Um dieses Problem zu lösen, kann bereits im Produktionscode das entsprechende Objekt der Klasse als Parameter übergeben werden. Der Test übergibt stattdessen das Mock-Objekt an die Klasse. Dieser Ablauf erhöht aber möglicherweise die Komplexität des Code. Eine andere Lösung ist es, das Objekt nicht im Produktionscode zu initialisieren, sondern diesen Vorgang an eine eigene Methode zu delegieren. Die Testklasse wiederum überschreibt diese Methode, um das Mock-Objekt zurückzugeben. Ein Beispiel dazu findet sich inListing 3.

Es gibt mehrere Module, die die Erstellung von Mock-Objekten erleichtern. Flexmock[2] ist sehr einfach anzuwenden, bietet aber auch nur wenige Funktionen. Wer mehr Flexibilität benötigt, ist bei Test::Unit::Mock[3] gut aufgehoben. Beide sind als Rubygem[4] verfügbar. Die Installation gestaltet sich bei einem bereits vorhandenen Rubygems-Paket sehr einfach: »gem install flexmock« oder »gem install test-unit-mock«.

Listing 3:
»test_article.rb«

01 require 'test/unit'
02 require 'article'
03 require 'rubygems'
04 require_gem 'flexmock'
05 
06 class Article
07    attr_accessor :mock
08    def createConnection
09       return @mock
10    end
11 end
12 
13 class TestArticle < Test::Unit::TestCase
14    def setup
15       @article = Article.new
16       @year, @month = 2004,5
17       @url_base = 'https://www.linux-magazin.de' + 
18       '/Artikel/ausgabe/' + sprintf('%s/%02d', @year, @month)
19       @no_content = 'keine verwertbaren Daten'
20       @html = <<html
21       <UL><li>Aktuell: News aus der Linux-Welt
22       <li><A href="userspace/userspace.html">Userspace-Dateisysteme</A>
23       </UL><UL><LI></UL>
24 html
25    end
26 
27    def test_parse_articles_no_content
28       assert_nil(@article.parse_articles(@no_content))
29    end
30 
31    def test_parse_articles
32       result = @article.parse_articles(@html)
33       assert_instance_of(Array, result)
34       assert_equal(2, result.length, 'falsche Anzahl an Artikeln')
35    end
36 
37    def test_load_article_page
38       mock = FlexMock.new
39       mock.mock_handle(:get) { [nil, 'testhtml'] }
40       @article.mock = mock
41       html = @article.load_article_page(@year, @month)
42       assert_equal('testhtml', html)
43    end
44 
45    def test_load_article_page_network_error
46       mock = FlexMock.new
47       mock.mock_handle(:get) { raise SocketError, 'Netzwerkfehler' }
48       @article.mock = mock
49       assert_raise(SocketError) { @article.load_article_page(@year,@month) }
50    end
51 
52    def test_get_address
53       url = @article.send(:get_address, @year, @month, 'suffix')
54       assert_equal(@url_base + 'suffix', url)
55    end
56 
57    def test_read_articles
58       mock = FlexMock.new
59       mock.mock_handle(:get) { |param|
60          assert_equal(@url_base + '/index_html?print=y', param, 'Falsche URL')
61          [nil, @html]
62       }
63       @article.mock = mock
64       result = @article.read_articles(@year, @month)
65       assert_equal('Userspace-Dateisysteme',result[1][0])
66       assert_equal(@url_base + '/userspace/userspace.html',result[1][1])
67       assert_equal(2, result.length, 'falsche Anzahl an Artikeln')
68    end
69 
70    def test_read_articles_no_content
71       mock = FlexMock.new
72       mock.mock_handle(:get) { [nil, @no_content] }
73       @article.mock = mock
74       assert_nothing_raised { @article.read_articles(@year, @month) }
75       assert_instance_of(String, @article.read_articles(@year, @month))
76    end
77 
78    def test_read_articles_network_error
79       mock = FlexMock.new
80       mock.mock_handle(:get) { raise SocketError, 'simulierter Netzwerkfehler' }
81       @article.mock = mock
82       assert_nothing_raised { @article.read_articles(@year, @month) }
83       assert_instance_of(String, @article.read_articles(@year, @month))
84    end
85 end

Internetzugriff testen

Ein Beispiel: Die Website des Linux-Magazins bietet die Inhaltsverzeichnisse aller Ausgaben seit 1994. Die Klasse »Article« (Listing 2) liest zu einer bestimmten Ausgabe die Titel der Artikel und – falls vorhanden – die URL des Artikelinhalts aus. Der Zugriff auf eine Quelle im Internet ist aber fehleranfällig und vergleichsweise langsam. Ein Mock-Objekt macht das Testbeispiel unabhängig von externen Ressourcen.

In der gekürzt abgebildeten Klasse »Article« in Listing 2 übernimmt die Methode »read_articles« Jahr und Monat des gewünschten Hefts und gibt eine Liste mit den Titeln und URLs der Artikel zurück. Der komplette Programmcode ist auf dem Server des Linux-Magazins verfügbar[6]. Die Testklasse in Listing 3 testet auf einige der wichtigsten Fehlerquellen wie fehlende Netzwerkverbindung oder nicht interpretierbare Rückgabewerte.

Fazit

Unit Testing ist nicht nur eine schöne Programmiertheorie. Es hilft tatsächlich dabei, Programme strukturierter und weniger fehleranfällig zu gestalten. Tools wie Zentest (siehe Kasten “Zentest”) unterstützen den Programmierer dabei, Methoden zu finden, die noch nicht durch einen Test abgedeckt sind.

Das Testen auf Randbedingungen und Extremsituationen entdeckt oft Fehler, die im Normalbetrieb schwierig zu interpretierende und vor allem schlecht nachvollziehbare Symptome erzeugen. Einmal gefundene Fehler mit einem Test zu dokumentieren garantiert, dass derselbe Fehler kein zweites Mal auftritt. Unit Testing kann also für jeden Entwickler ein nützliches Werkzeug zur Steigerung der Produktivität sein. (ofr)

Zentest

Auch wenn immer wieder empfohlen wird, Tests bereits während des Programmierens oder sogar noch vor dem Beginn zu schreiben, sind in der Praxis oft Programme zu finden, die zwar funktionieren, aber wenige oder überhaupt keine Tests enthalten. Da die Tests aber vor allem für das Refactoring eines schlecht strukturierten Programms wichtig sind, soll nun eine möglichst gute Abdeckung des Programms durch Unit-Tests Abhilfe schaffen. Zentest hilft dabei, indem es aus einer vorhandenen Programmdatei eine passende Testdatei erzeugt.

Zentest kann auch während der Entwicklung eines bereits getesteten Programms die Programm- und Testdatei analysieren und daraus eine Datei erzeugen, die alle fehlenden Methoden auflistet, egal ob im Test oder im eigentlichen Programm. Wenn Zentest per Parameter die Testdatei übermittelt bekommt, wird auch gleich der Test ausgeführt.

  • »ZenTest klasse.rb« gibt alle nötigen
    Testmethoden für die Klasse aus.
  • »ZenTest test_klasse.rb« gibt alle nötigen
    Methoden für den Produktionscode aus.
  • »ZenTest klasse.rb test_klasse.rb« vergleicht die
    Klasse mit der Testklasse und gibt die Differenz aus.

Damit Zentest richtig funktioniert, ist es wichtig, sich an die Namenskonventionen zu halten. Jeder Methode » foo« steht eine Testmethode »test_ foo« gegenüber. Um verschiedene Fälle testen zu können, ordnet Zentest auch alle Testmethoden, an die noch ein Unterstrich und ein beliebiger Text angehängt wird, derselben Methode zu: »test_foo _Erweiterung«. Weitere Informationen zu Zentest liefert die Website[5].

Infos

[1] Test::Unit-Dokumentation: [http://www.ruby-doc.org/stdlib/libdoc/test/unit/rdoc/index.html]

[2] Flexmock: [http://onestepback.org/software/flexmock/]

[3] Test::Unit::Mock: [http://www.deveiate.org/code/Test-Unit-Mock.html]

[4] Rubygems: [http://rubygems.rubyforge.org/wiki/wiki.pl?RubyGems]

[5] Zentest: [http://www.zenspider.com/ZSS/Products/ZenTest/]

[6] Listings online: [https://www.linux-magazin.de/Service/Listings/2005/04/Ruby/]

Der Autor

Michael Raidel arbeitet in Salzburg als Programmierer mit dem Schwerpunkt Webapplikationen. Dazu benutzt er die Programmiersprachen PHP, Ruby und Python.

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben