let’s dev

Blog

Top!

let’s dev | Scroll to top
let’s dev | Scroll to next content item

Hello

let’s dev | Scroll to previous content item
let’s dev Blog | Automatisiertes Testing von C++ Code mit Frameworks – Teil 1
by Arne
20. Februar 2019

Automatisiertes Testing von C++ Code mit Frameworks – Teil 1

Das Testen von Applikationen ist auch im mobilen Umfeld ein wichtiger Bestandteil des Entwicklungsprozesses. Bei Bibliotheken, die in C++ geschrieben sind, wird dies durch gtest und gmock ermöglicht. Wir stellen die Verwendung an einfachen Beispielen vor.

Entwicklungstests

Automatisierte Tests sind aus modernen Entwicklungsprozessen nicht mehr wegzudenken. Sie tragen nicht nur dazu bei, die Qualität der Software sicherzustellen, sondern ermöglichen es dem Entwickler auch, Fehler und Probleme frühzeitig zu entdecken und zu beheben.

Hierbei sind verschiedene Arten von Tests zu unterscheiden. Als Beispiele seien Unit-Tests zum Testen einer Komponente und Integration-Tests, die das Zusammenspiel mehrerer Komponenten prüfen, genannt. Für die meisten Programmiersprachen gibt es zudem Frameworks, die das Erstellen und Ausführen bedeutend erleichtern, so auch für C++.

Frameworks gtest und gmock

Die Google Frameworks gtest und gmock wurden zunächst in separaten Projekten entwickelt, inzwischen jedoch zusammengeführt. gtest ist ein Test-Framework für C++, welches das Testen und Asserten von Anwendungen erleichtern soll. Beinhaltete Funktionalitäten sind z.B. die Organisation der Tests in Test Cases und das Anlegen von Fixtures zur Wiederverwendung. Mit gmock können dagegen Mock-Objekte erstellt werden, um z.B. zu testen, ob eine Funktion mit den erwarteten Parametern aufgerufen werden kann. Im ersten Teil des Beitrags, soll zunächst näher auf das Framework gtest eingegangen werden.

gtest und gmock im Vergleich mit anderen C++ Unit Testing Frameworks

Die Vielzahl von C++ Unit Testing Frameworks am Markt erschwert die richtige Wahl erheblich. Daher ist es sinvoll, die Frameworks im Vorfeld unter verschiedenen Gesichtspunkten miteinander zu vergleichen. Bereits 2004 wurde einige dieser Frameworks wie CppUnit oder Boost.Test gegenübergestellt. Auch wenn gtest zum damaligen Zeitpunkt noch nicht existiert hat, ist es doch interessant, einen Blick auf die verwendeten Metriken zu werfen:

Diese Fragen sollen nun im Verlauf dieses Beitrags beantwortet werden. Bereits vorab ist zu sagen, dass gtest alle genannten Anforderungen erfüllt. Trotz allem ist die Wahl des Frameworks immer abhängig davon, in welchem Projekt es zum Einsatz kommen soll und muss individuell festgelegt werden.

gtest

Dieser Beitrag beschäftigt sich zwar mit gtest, jedoch sind alle allgemeinen Themen auch für gmock gültig, da die Funktionalität in einer Bibliothek zusammengeführt wurde.

Zur Beschreibung muss eine eindeutige Nomenklatur festgelegt werden. gtest verwendet für einzelne Tests die Bezeichnung TEST() bzw. Test, die Gruppierung von Tests für eine Komponente wird Test Case genannt.Ein Begriff der im weiteren Verlauf häufiger auftauchen wird, ist der des Test Fixtures. Dieser bezeichnet eine Klasse, die Funktionen und Daten enthält, welche in mehreren Tests wiederverwendet werden.

Setup

Das Framework funktioniert auf einer Vielzahl von Plattformen sowie mit verschiedenen Compilern. Zu den unterstützten Systemen zählen u.a. Windows, Mac OS X und Linux. Es ist möglich, gtest als statische und dynamische Library zu bauen und zu linken. Die Verwendung von cmake vereinfacht die Integration. So lässt sich gtest als Stand-Alone bauen oder auch in andere Projekte integrieren.

add_subdirectory (${ CMAKE_CURRENT_BINARY_DIR }/ googletest -src ${
CMAKE_CURRENT_BINARY_DIR }/ googletest - build EXCLUDE_FROM_ALL )

Vor der eigentlichen Implementierung, wie oben dargestellt, muss der Download von GitHub erfolgen. Hierbei besteht die Möglichkeit, den Code direkt zu integrieren oder immer als Dependency automatisiert zu laden.

Allgemeine Struktur

Prinzipiell ist es sinnvoll, Tests ähnlich der Projektstruktur aufzubauen. Das macht es für andere Entwickler leichter, Tests für eine bestimmte Klasse zu finden. Auch kann so schneller herausgefunden werden, ob für eine Klasse Tests vorliegen. Tests, welche dieselben Ressourcen benötigen, sollten in einer Datei geschrieben werden. Dies macht es sehr einfach, die Fixtures einzusetzen und die Ressourcen nur an einer Stelle vorzubereiten.

Testbeispiele

Ein einfacher Test wird in einem Makro geschrieben:

TEST ( TestCaseName , TestName ) {
                ... test body ...
}

Der TestCaseName sowie der TestName sind frei wählbar. Die Namen der Tests dürfen sich auch in verschiedenen TestCases wiederholen, nur innerhalb eines TestCases muss der TestName eindeutig sein.

Fixture Tests dienen dazu, in verschiedenen Testfällen auf gleiche Ressourcen zugreifen zu können. Das kann ein Dummy Objekt sein, welches mit Testdaten gefüllt wird, es kann sich aber auch um eine komplexe Initialisierung handeln. Um diese Funktionalität verwenden zu können, muss zuvor eine Klasse angelegt werden, welche dann als Fixture dient.

(1) Test Fixture MyFixture.h
# include <gtest / gtest .h>
class MyFixture : public testing :: Test {
protected :
void SetUp () override ;
bool myFunction ();
};
(2) Test Fixture MyFixture.cpp
void BaseTest :: SetUp () {
Test :: SetUp ();
... my additional setup ...
}
TEST_F ( MyFixture , TestName ) {
bool myBool = myFunction ();
... my aditional testing ...
}

Im Header hat die Funktion „myFunction“ die Sichtbarkeit „protected“. Alle Funktionen und Eigenschaften, die im Testmakro aufgerufen werden, müssen „protected“ oder „public“ sichtbar sein. Die SetUp Funktion kann verwendet werden, um komplexe Initialisierungen durchzuführen und wird vor jedem Test ausgeführt. Analog kann auch eine „TearDown“ implementiert werden. Das Makro in (2) hat sich im Vergleich zu (1) leicht verändert. Aus TEST ist TEST_F geworden. Das F steht dabei für Fixture. Der Name der Fixture kann frei gewählt werden, er muss nur in der Fixture Klasse und im Makro identisch sein.

Überprüfungen (Assertions)

Assertions zählen zu den wichtigsten Funktionen jedes Test-Framewors. gtest stellt eine Anzahl verschiedenster Überprüfungen zur Verfügung mit deren Hilfe Werte oder Objekte verglichen werden können.

Assertions gibt es als fatalen oder nicht fatalen Check. Bei einer fatalen Überprüfung wird der aktuell ausgeführte Test abgebrochen, falls der Vergleich als falsch bewertet wird. Ein nicht fataler Check lässt den Test zu Ende laufen, markiert ihn jedoch anschließend als fehlgeschlagen.

Fatale Vergleiche beginnen mit „ASSERT_“, nicht fatale mit „EXPECT_“. Wenn möglich, sollten immer nicht fatale Überprüfungen erfolgen.

In den folgenden Beispielen ist die Verwendung einiger Überprüfungen dargestellt. Es handelt sich jedoch nur um einen kleinen Ausschnitt, der von Google bereitgestellten Funktion.

(1) Verwendung nicht fataler Überprüfungen
EXPECT_TRUE ( MyClass :: myBoolFunc ());
std :: string expectedResult (" this result ");
EXPECT_STREQ ( expectedResult . c_str () , MyClass :: myStringFunc (). c_str ());
EXPECT_EQ (42 , MyClass :: myIntFunc ());
EXPECT_GT (0, MyClass :: myLongFunc ());

Die verschiedenen Makros in (1) testen mehrere Bedingungen. Das Makro mit _STREQ überprüft zwei Strings auf ihre Gleichheit, über _EQ wird die Gleichheit von Zahlen abgeglichen und _GT überprüft, ob die erste Zahl größer als die zweite ist.

(2) Verwendung fataler und nicht fataler Überprüfungen
User * user = MyClass :: getMyObject ();
ASSERT_NE ( nullptr , user );
EXPECT_EQ (25 , user ->age);
EXPECT_EQ (female , user -> gender );

In (2) wird die Existenz des Objektes mit ASSERT_NE geprüft. Natürlich ist es nicht sinnvoll, den Test weiterlaufen zu lassen, wenn das Objekt nicht existiert. Die Überprüfung des Alters erfolgt hingegen über EXPECT_EQ, da hier auch weitere Eigenschaften geprüft werden können, auch wenn das Alter nicht korrekt ist.

Im Folgenden soll nicht immer auf beide Varianten eingegangen werden. Wenn also die Rede von Assert oder Expect ist, sind, wenn nicht explizit erwähnt, immer beide Varianten gemeint.

Es ist möglich, einer Überprüfung eine individuelle Ausgabe zu übergeben. Diese wird im Fall eines Fehschlages ebenfalls im Log ausgegeben und macht es leichter, die Ursache zu identifizieren. Dazu wird die Nachricht mit Hilfe des Stream Operators « an die Assertion angehangen.

TEST_F ( MyTestFixture , checkitemCount ) {
int itemCount = _sut -> getItemCount ();
EXPECT_EQ (0, itemCount ) << " Item count was not the expected size .";
}

Death Tests

Mit Death Tests kann geprüft werden, ob das Programm bei einer bestimmten Ausführung abstürzt. Warum aber sollte man dies testen wollen? In C++ ist es üblich, Vorbedingungen durch Checks mit abort() abzusichern. Insbesondere innerhalb der st1 oder anderer Bibliotheken wird dieser Art der Sicherung verwendet.

Um Death Tests zu verwenden, muss innerhalb des Tests der Tod bzw. Absturz des Programms erwartet werden:

EXPECT_DEATH (_sut -> myDeadlyFunction () , ".* this did happen .*")

Bei der Verwendung des Makros kommen zwei Parameter zum Einsatz. Im ersten Parameter muss der Aufruf definiert werden, der das Programm zum Abstürzen bringen soll. Das zweite Argument ist eine Regex, die du zu erwartende Fehlermeldung beschreibt. Der Test schlägt so unter zwei Bedingungen fehl: Zum einen wenn das Programm beim Aufruf von myDeadlyFunction nicht abstürzt, zum anderen wenn die Fehlermeldung nicht die übergebene Regex erfüllt.

Warum ist die Fehlermeldung entscheidend? Wenn die Software nicht mit der erwarteten Meldung terminiert ist, wurde nicht die Logik angesprochen, die eigentlich getestet werden sollte. Eine Bedingung war also nicht erfüllt oder ein Systemfehler ist vorher bzw. nachher aufgetreten. Somit ist unklar, ob die zu testende Bedingung erfüllt ist. Die Regex ermöglicht es, das Programm exakt an der zu erwartenden Stelle zu terminieren.

Exception Tests

Im Grund handelt es sich hierbei um eine Gruppe von Asserts und nicht um eine eigene Testart. Da Exceptions aber auch für C++ immer mehr an Bedeutung gewinnen, sei darauf hingewiesen, dass auch bei diesen eine Überprüfung mit gtest möglich ist. Die Assertions gibt es ebenfalls als fatale und nicht fatale Variante.

(1) Funktion mit Exception
EXPECT_THROW (_sut -> myThrowingFunc () , std :: exception );
ASSERT_THROW (_sut -> myThrowingFunc () , std :: exception );
(2) Funktion ohne Exception
EXPECT_NO_THROW (_sut -> myNotThrowingFunc ());
ASSERT_NO_THROW (_sut -> myNotThrowingFunc ());

Floating Point Vergleiche

Aufgrund interner Darstellungen und Rundungsproblemen bei Gleitkommazahlen ist ein Vergleich dieser meist schwierig, denn zwei Gleitkommazahlen werden fast nie den exakt selben Wert besitzen. Daher wird der Vergleich mit ASSERT_EQ fehlschlagen. Zur Lösung dieses Problems gibt ein spezifisches Makro zum Vergleichen von Gleitkommazahlen. Diese gleichen auf vier Units in the Last Place (ULP) ab. Es sind spezifische Versionen für float und double verfügbar:

ASSERT_FLOAT_EQ (0.0f, _sut -> myFloat ());
ASSERT_DOUBLE_EQ (0.0 , _sut -> myDouble ());
EXPECT_FLOAT_EQ (0.0f, _sut -> myFloat ());
EXPECT_DOUBLE_EQ (0.0 , _sut -> myDouble ());

Assertion mit Matchern

gmock bringt eigene Matcher zu gtest. Da es möglich ist, eigene Matcher zu schreiben, können auch individuelle Vergleiche erstellt werden. Es können also komplexe Vergleiche in Matchern ausgelagert und mit ordentlichen Fehlermeldungen versehen werden. Auch eine Mehrfachverwendung der Matcher ist denkbar.

Will man zum Beispiel den Inhalt eines Vektors von Pointern vergleichen, gibt es dafür keinen Standardvergleich. Der Nutzer muss also manuell den Vektor iterieren und den Inhalt vergleichen. Hat man mehrere Tests, die diese Vektoren vergleichen, entsteht schnell viel doppelter Code. Wird der Code in Funktionen ausgelagert, so erhält man unübersichtliche Tests. Mittels Matchern lässt sich eine mit gtest funktionierende Vergleichsoperation für die Vektoren erstellen.Ein einfaches Beispiel hierfür sieht wie folgt aus:

MATCHER_P ( vectorContentEq , expectedVector , " Expected vector content does
not match .") {
bool result = (arg. size () == expectedVector . size ());
for ( int i = 0; i < std :: min(arg . size () , expectedVector . size ()); ++i) {
auto firstItem = arg.at(i);
auto secondItem = expectedVector .at(i);
if( firstItem != secondItem ){
result = false ;
break ;
}
}
return result ;
}

MATCHER_P definiert einen neuen Matcher mit dem Namen vectorContentEq. Das P steht für einen Matcher, der einen Parmater erwartet. In diesem Fall ist der Parameter expectedVector. Es werden die Länge der Vektoren und der Inhalte der Elemente verglichen.

Es sind zudem mehr Matcher als Assertion Makros vorhanden. Es gibt also Situationen, in denen das Testen mit den von Google bereitgestellten Matchern sinnvoller ist als mit Makros. So können manche Matcher etwa Teile von Strings vergleichen:

EXPECT_THAT ( myString , StartsWith ("My string Starts with "));

Zur Verwendung von Matchern wird das Makro EXPECT_THAT verwendet und im zweiten Argument der Matcher übergeben. Matcher sind jedoch keine einzigartige Implementierung von gmock. Ein ähnliches Konzept kann in neueren jUnit Versionen gefunden werden. Auch dort wird der Ausdruck Matcher verwendet.

Parametrisierte Tests

Funktionen mit Eingabeparameter zu testen, gehört zu den alltäglichen Aufgaben eines Entwicklers. Soll sich die Funktion bei verschiedenen Parametern gleich verhalten, so muss derselbe Test mehrfach geschrieben werden. Bei gtest gibt übergibt man einem Test Parameter und legt anschließend fest, mit welchen Werten in dem Parameter die Tests ausgeführt werden sollen.

Definition eines parametrisierten TestCases
TEST_P ( MyParamTest , TestName ) {
std :: string myParam = GetParam ();
... normal testing with usage of myParam ...
}

Es sind einige kleine Unterschiede zu erkennen. Das Makro endet auf P für parameterised Test. Bei der Implementierung wurde der Aufruf GetParam() verwendet, um auf den Parameter zugreifen zu können. Der Parameter wird aus Gründen der Lesbarkeit einer lokalen Variable zugewiesen. Diese Vorgehensweise ist jedoch nicht zwingend erforderlich. Wenn die Testklasse nun ausgeführt wird, ist kein Ergebnis zu erwarten. Grund dafür ist, dass zunächst noch definiert werden muss, mit welchen Werten die Tests ausgeführt werden sollen. Mehrere Tests können dabei problemlos mit denselben Parametern ausgeführt werden.

Definition der parametrisierten Werte
INSTANTIATE_TEST_CASE_P ( myParamTestInstance , MyParamTest , :: testing ::
Values (" value1 ", " value2 ", " value3 "));

Nun werden alle Tests im Test Case „MyParamTest“ dreimal ausgeführt, einmal mit jedem Parameter.

Platzierung von Assertions

Im Gegensatz zu anderen Frameworks können Assertions bei gtest in nahezu beliebige Funktionen geschrieben werden. Es müssen hierfür lediglich zwei Bedingungen erfüllt sein. Zum einen muss der Include-Befehl von gtest enthalten sein, zum anderen müssen fatale Assertions gesondert behandelt werden. Dies betrifft also alle mit „ASSERT_“ beginnenden sowie das direkte Makro „Fail“, die nur in Funktionen mit dem Rückgabetyp „void“ eingesetzt werden können.

Assertions in Funktionen

Es ist gute Praxis, sich wiederholende Blöcke von Assertions in Hilfsklassen auszulagern. Ein Beispiel hierfür ist etwa ein User Objekt mit mehreren Eigenschaften. Möchte man dieses überprüfen, so stehen eine ganze Reihe von Assertions zur Verfügung. Es liegt auf der Hand, diesen Block in eine Hilfsklasse bzw. Hilfsfunktion auszulagern. Jedoch ergibt sich hierdurch das Problem, dass die eventuelle Ausgabe einer fehlgeschlagenen Assertion nicht mehr sonderlich aussagekräftig ist. Denn es ist nicht mehr auf den ersten Blick ersichtlich, aus welchem Test heraus die Assertion fehlgeschlagen ist.

gtest hat für diesen Fall das Makro „SCOPED_TRACE“ eingeführt, das dem log im aktuellen Kontext eine Nachricht mit Datei und Zeilennummer anfügt. Wird der aktuelle Scope verlassen, wird auch die Nachricht entfernt. Für das Beispiel von eben sieht das wie folgt aus:

TEST_F ( MyTestFixture , testUserWithSomething ) {
if( something ()){
SCOPED_TRACE (" Something ");
User * user = sut -> getUser ();
testUser ( user );
}
User * user = sut -> getUser ();
testUser ( user );
}

Ohne die „SCOPED_TRACE“ Makros wäre es bei Assertion Fehlern in der „testUser“ Funkion sehr schwer herauszufinden, welcher der beiden Fälle zu dem Failure geführt hat. Mit dem Makro jedoch würde bei einem Fehlschlag im „if“ ein Log dieser Art angezeigt:

path / MyTestFixture .cpp :198: Something

Schlägt der Test jedoch erst bei dem zweiten Anruf fehl, so ist die Nachricht im Log nicht vorhanden.

Testausgabe verändern

Die Standardausgabe von gtest sieht wie folgt aus:

... a lot mor tests and logs ...
[ OK ] MyTestCase . myTest (4 ms)
[----------] 4 tests from DeviceOnboardingManagerTest (18 ms total )
[----------] Global test environment tear - down
[==========] 452 tests from 40 test cases ran. (16823 ms total )
[ PASSED ] 452 tests .

Problematisch ist hierbei jedoch die Tatsache, dass die gtest Ausgaben mit den eigentlichen Ausgaben auf „std::count“ vermischt werden können. Es ist jedoch möglich, diese Ausgaben zu verändern. Folgende Anpassungen können vorgenommen werden:

An dieser Stelle sei außerdem erwähnt, dass gtest von integrierten Entwicklungsumgebungen unterstützt wird. So ist es beispielsweise möglich, nur einzelne Tests oder Test Cases auszuführen. In der Entwicklungsumgebung CLion sieht das wie folgt aus:

Abbildung 1: gtest in CLion

Abbildung 1: gtest in CLion

Einschränkungen

gtest gilt auf Systemen, die „pthreads“ zur Verfügung stellen, als Threadsafe. Das Attribut gibt Auskunft darüber, ob es sicher ist, Programmcode von verschiedenen Threads aufzurufen. Auf anderen Systemen wie etwa Windows ist es derzeit jedoch nicht sicher, Assertions von zwei Threads auszuführen. Für die meisten Applikationen ist dies jedoch nicht von Bedeutung.


Im zweiten Teil der Blogreihe stellen wir das Mock Framework gmock vor und geben unsere Einschätzung zum automatisierten Testen mit den beiden vorgestellten Frameworks ab. Stay tuned!

Weitere Artikel aus unserem Blog

let’s dev Blog | Automatisiertes Testing von C++ Code mit Google Test und Google Mock – Teil 2

Technical

Automatisiertes Testing von C++ Code mit Google Test und Google Mock – Teil 2

by Arne

2019-06-13

Weiterlesen
let’s dev Blog | Apple WWDC 2019: Das sind die Highlights der Keynote

Technical

Apple WWDC 2019: Das sind die Highlights der Keynote

by Nicolas

2019-06-05

Weiterlesen
let’s dev Blog | App Builders 2019

Technical

App Builders 2019

by Nicolas

2019-05-23

Weiterlesen
let’s dev Blog | Offizielle Eröffnung des Festigungs- und Expansionszentrums (FUX)

Corporate

Offizielle Eröffnung des Festigungs- und Expansionszentrums (FUX)

by Helena

2019-04-15

Weiterlesen
let’s dev Blog | Delegation aus Nottingham zu Besuch auf dem Alten Schlachthof in Karlsruhe

Corporate

Delegation aus Nottingham zu Besuch auf dem Alten Schlachthof in Karlsruhe

by Helena

2019-04-14

Weiterlesen
let’s dev Blog | Es ist soweit: Wir ziehen um!

Corporate

Es ist soweit: Wir ziehen um!

by Helena

2019-03-26

Weiterlesen
let’s dev Blog | Die App im Google Play Store

Technical

Die App im Google Play Store

by Elisa

2019-01-24

Weiterlesen
let’s dev Blog | „UX Day“ 2018

Corporate

„UX Day“ 2018

by Aileen

2018-12-17

Weiterlesen
let’s dev Blog | let's dev unterstützt SG Siemens Volleyballer aus Karlsruhe

Corporate

let's dev unterstützt SG Siemens Volleyballer aus Karlsruhe

by Helena

2018-12-04

Weiterlesen
let’s dev Blog | KMU gestalten die Digitalisierung – Mittelstandskonferenz 2018

Corporate

KMU gestalten die Digitalisierung – Mittelstandskonferenz 2018

by Helena

2018-11-12

Weiterlesen
let’s dev Blog | Apple Wallet

Technical

Apple Wallet

by Maik

2018-10-26

Weiterlesen
let’s dev Blog | „Mensch und Computer“ 2018

Corporate

„Mensch und Computer“ 2018

by Judith

2018-09-24

Weiterlesen
let’s dev Blog | State Design Pattern in Android

Technical

State Design Pattern in Android

by Thomas

2018-09-17

Weiterlesen
let’s dev Blog | let’s dev ist autorisiertes Beraterunternehmen im Förderprogramm „go-digital“

Corporate

let’s dev ist autorisiertes Beraterunternehmen im Förderprogramm „go-digital“

by Helena

2018-09-01

Weiterlesen
let’s dev Blog | App Design & Development Conference 2018

Corporate

App Design & Development Conference 2018

by Helena

2018-08-14

Weiterlesen
let’s dev Blog | iOS 12: Die Top-Neuerungen im Überblick

Technical

iOS 12: Die Top-Neuerungen im Überblick

by Nicolas

2018-07-17

Weiterlesen
let’s dev Blog | let’s dev auf der CEBIT

Corporate

let’s dev auf der CEBIT

by Karl

2018-06-11

Weiterlesen
let’s dev Blog | Einführung in User Interface (UI) Tests mit Espresso

Technical

Einführung in User Interface (UI) Tests mit Espresso

by Raphael

2018-06-07

Weiterlesen
let’s dev Blog | Die App im Apple App Store: welche Informationen werden benötigt?

Technical

Die App im Apple App Store: welche Informationen werden benötigt?

by Aileen

2018-04-27

Weiterlesen
let’s dev Blog | Smart Pointer in C++

Technical

Smart Pointer in C++

by Matthias

2018-04-01

Weiterlesen
let’s dev Blog | User Interface Design für das iPhone X: alle Neuerungen auf einen Blick

Technical

User Interface Design für das iPhone X: alle Neuerungen auf einen Blick

by Helena

2018-02-07

Weiterlesen
let’s dev Blog | WebVR – Virtual Reality Experience im Browser mit dem A-Frame Framework

Technical

WebVR – Virtual Reality Experience im Browser mit dem A-Frame Framework

by Judith

2018-01-10

Weiterlesen
let’s dev Blog | Open Data Hackathon der Deutschen Bahn

Corporate

Open Data Hackathon der Deutschen Bahn

by Karl

2015-03-31

Weiterlesen
let’s dev Blog | Blur-Effekte unter iOS 7

Technical

Blur-Effekte unter iOS 7

by Katja

2014-04-24

Weiterlesen
let’s dev Blog | Beyond App Store – Distribution von iOS-Anwendungen

Technical

Beyond App Store – Distribution von iOS-Anwendungen

by Karl

2012-08-27

Weiterlesen
let’s dev Blog | Frontend-Architektur – Model View Presenter und Message Bus

Technical

Frontend-Architektur – Model View Presenter und Message Bus

by Karl

2011-03-08

Weiterlesen