Sollten wir alle unsere Methoden testen?


61

Also habe ich heute mit meinem Teamkollegen über Unit-Tests gesprochen. Das Ganze begann, als er mich fragte: "Hey, wo sind die Tests für diese Klasse, ich sehe nur eine?". Die ganze Klasse war ein Manager (oder ein Service, wenn Sie es so nennen möchten) und fast alle Methoden delegierten einfach Dinge an ein DAO, sodass es ungefähr so ​​aussah:

SomeClass getSomething(parameters) {
    return myDao.findSomethingBySomething(parameters);
}

Eine Art Boilerplate ohne Logik (oder zumindest halte ich eine so einfache Delegation nicht für Logik), aber in den meisten Fällen eine nützliche Boilerplate (Schichtentrennung usw.). Und wir hatten eine ziemlich lange Diskussion, ob ich es testen sollte oder nicht (ich denke, es ist erwähnenswert, dass ich das DAO vollständig getestet habe). Seine Hauptargumente sind, dass es nicht (offensichtlich) TDD war und dass jemand den Test sehen möchte, um zu überprüfen, was diese Methode tut (ich weiß nicht, wie es offensichtlicher sein könnte) oder dass in Zukunft jemand das ändern möchte Implementierung und Hinzufügen neuer (oder ähnlicher) Logik (in diesem Fall sollte jemand diese Logik einfach testen ).

Das brachte mich jedoch zum Nachdenken. Sollten wir uns um die höchste Testabdeckung bemühen? Oder ist es nur Kunst um der Kunst willen? Ich sehe einfach keinen Grund für das Testen von Dingen wie:

  • Getter und Setter (es sei denn, sie haben tatsächlich eine Logik in sich)
  • Code "Boilerplate"

Natürlich würde ein Test für eine solche Methode (mit Mocks) weniger als eine Minute dauern, aber ich denke, das ist immer noch Zeitverschwendung und eine Millisekunde länger für jedes CI.

Gibt es vernünftige / nicht "brennbare" Gründe, warum man jede einzelne (oder so viele wie möglich) Codezeile testen sollte?


2
Ich entscheide mich immer noch für diese Frage, aber hier ist ein Vortrag von jemandem, der entschieden hat, dass die Antwort "nein" ist. Ian Cooper: TDD, wo ist alles schief gelaufen? Um dieses großartige Gespräch zusammenzufassen, sollten Sie von außen nach innen testen und neue Verhaltensweisen testen, nicht neue Methoden.
Daniel Kaplan

Dies ist wirklich ein großartiges Gespräch, ein Muss, ein aufschlussreiches Gespräch für viele Menschen, ich liebe es. Aber ich denke, dass die Antwort nicht "nein" ist. Es ist "ja, aber indirekt". Ian Cooper spricht über sechseckige Architektur und Testfunktionen / -verhalten, die die Ports verspotten / blockieren. In diesem Fall handelt es sich bei diesen Ports um die DAOs und diesen "Manager / Service", der nicht mit einem einzelnen Komponententest nur für diese Klasse getestet wurde, sondern mit einem "Komponententest" (Einheit in der Ian Cooper-Definition, der ich voll und ganz zustimme), der eine Funktion testet in Ihrer Domain, die diesen Manager / Dienst verwenden.
AlfredoCasado


Es hängt in gewissem Maße von Ihrem System ab. Wenn Sie ein System mit einem mittleren bis hohen Sicherheitsniveau entwickeln, müssen Sie alle Methoden unabhängig von der Trivialität abdecken
jk.

Antworten:


48

Ich halte mich an Kent Becks Faustregel:

Testen Sie alles, was brechen könnte.

Das ist natürlich einigermaßen subjektiv. Für mich sind Trivial Getters / Setter und One-Liner wie Ihres normalerweise nicht wert. Andererseits verbringe ich die meiste Zeit damit, Komponententests für Legacy-Code zu schreiben, und träume nur von einem schönen TDD-Projekt auf der grünen Wiese ... Bei solchen Projekten gelten andere Regeln. Das Hauptziel des Legacy-Codes ist es, mit möglichst geringem Aufwand so viel wie möglich zu bearbeiten. Daher sind Komponententests in der Regel komplexer und komplexer als Integrationstests, wenn die Terminologie umständlich ist. Und wenn Sie Schwierigkeiten haben, die allgemeine Codeabdeckung von 0% zu steigern, oder es nur geschafft haben, sie auf über 25% zu steigern, sind Unit-Testing-Tests die geringste Ihrer Sorgen.

OTOH In einem TDD-Projekt auf der grünen Wiese ist es möglicherweise sachlicher, Tests auch für solche Methoden zu schreiben. Zumal Sie den Test bereits geschrieben haben, bevor Sie die Chance bekommen, sich zu fragen: "Ist diese eine Zeile einen speziellen Test wert?". Und zumindest sind diese Tests einfach zu schreiben und schnell auszuführen, so dass es auch keine große Sache ist.


Ah, ich habe dieses Zitat total vergessen! Ich schätze, ich werde es als Hauptargument verwenden, weil offen gesagt - was kann hier brechen? Nicht wirklich viel. Das einzige, was kaputt gehen kann, ist der Methodenaufruf, und wenn das passiert, bedeutet das, dass etwas wirklich Schlimmes passiert ist. Vielen Dank!
Zenzen

5
@Zenzen: "Was kann hier brechen? Nicht wirklich viel." - So kann es brechen. Nur ein kleiner Tippfehler. Oder jemand fügt Code hinzu. Oder vermasselt die Abhängigkeit. Ich denke wirklich, dass Beck behaupten würde, dass Ihr Hauptbeispiel als zerbrechlich qualifiziert ist. Getter und Setter, weniger, obwohl ich mich selbst dann in einem Kopier- / Einfügefehler befunden habe. Die eigentliche Frage ist: Wenn es zu trivial ist, einen Test zu schreiben, warum gibt es ihn überhaupt?
pdr

1
Die Zeit, die Sie damit verbracht haben, darüber nachzudenken, hätte man auch schreiben können. Ich sage, schreibe den Test, lasse es nicht zu, wenn du keinen Test schreibst, da eine graue Fläche erscheint und mehr zerbrochene Fenster erscheinen.
kett_chup 20.01.12

1
Ich füge hinzu, meine allgemeine Erfahrung ist, dass das Testen von Gettern und Setzern auf lange Sicht etwas wertvoll ist, aber eine niedrige Priorität hat. Der Grund dafür ist, dass die Wahrscheinlichkeit, einen Fehler zu finden, "Null" ist. Sie können nicht garantieren, dass ein anderer Entwickler in drei Monaten nichts hinzufügt ("nur eine einfache if-Anweisung"), bei dem die Gefahr besteht, dass es zu einem Absturz kommt . Ein Unit-Test schützt dagegen. Gleichzeitig hat es nicht wirklich eine zu hohe Priorität, weil Sie auf diese Weise nicht bald etwas finden werden.
Erklärungen

7
Blind alles zu testen, was brechen könnte, macht keinen Sinn. Es muss eine Strategie geben, bei der Komponenten mit hohem Risiko zuerst getestet werden.
CodeART

12

Es gibt einige Arten von Unit-Tests:

  • Zustand basiert. Sie handeln und behaupten dann gegen den Zustand des Objekts. ZB mache ich eine Einzahlung. Ich überprüfe dann, ob sich das Gleichgewicht erhöht hat.
  • Rückgabewert basiert. Sie handeln und behaupten gegen den Rückgabewert.
  • Interaktion basiert. Sie stellen sicher, dass Ihr Objekt ein anderes Objekt aufgerufen hat. Dies scheint das zu sein, was Sie in Ihrem Beispiel tun.

Wenn Sie Ihren Test zuerst schreiben würden, wäre dies sinnvoller, da Sie erwarten würden, dass Sie eine Datenzugriffsschicht aufrufen. Test würde zunächst fehlschlagen. Sie würden dann Produktionscode schreiben, um den Test zu bestehen.

Idealerweise sollten Sie logischen Code testen, aber Interaktionen (Objekte, die andere Objekte aufrufen) sind ebenso wichtig. In Ihrem Fall würde ich

  • Überprüfen Sie, ob ich die Datenzugriffsebene mit dem genauen Parameter aufgerufen habe, der übergeben wurde.
  • Stellen Sie sicher, dass es nur einmal aufgerufen wurde.
  • Überprüfen Sie, ob ich genau das zurückgebe, was mir von der Datenzugriffsebene gegeben wurde. Andernfalls kann ich auch null zurückgeben.

Derzeit gibt es dort keine Logik, aber das wird nicht immer der Fall sein.

Wenn Sie jedoch sicher sind, dass diese Methode keine Logik enthält und wahrscheinlich dieselbe bleibt, würde ich in Betracht ziehen, die Datenzugriffsebene direkt vom Verbraucher aufzurufen. Ich würde das nur tun, wenn der Rest des Teams auf der gleichen Seite ist. Sie möchten keine falsche Nachricht an das Team senden, indem Sie sagen: "Hey Leute, es ist in Ordnung, die Domänenschicht zu ignorieren, rufen Sie einfach die Datenzugriffsschicht direkt an."

Ich würde mich auch darauf konzentrieren, andere Komponenten zu testen, wenn es einen Integrationstest für diese Methode gäbe. Ich bin jedoch noch nicht bei einem Unternehmen mit soliden Integrationstests.

Nach alledem würde ich nicht alles blind testen. Ich würde die Hot Spots feststellen (Bauteile mit hoher Komplexität und hoher Bruchgefahr). Ich würde mich dann auf diese Komponenten konzentrieren. Es hat keinen Sinn, eine Codebasis zu haben, bei der 90% der Codebasis recht einfach ist und durch Komponententests abgedeckt werden, während die verbleibenden 10% die Kernlogik des Systems darstellen und aufgrund ihrer Komplexität nicht durch Komponententests abgedeckt werden.

Was ist der Vorteil des Testens dieser Methode? Was sind die Auswirkungen, wenn dies nicht funktioniert? Sind sie katastrophal? Bemühen Sie sich nicht um eine hohe Codeabdeckung. Die Codeabdeckung sollte ein Nebenprodukt einer guten Reihe von Komponententests sein. Beispielsweise können Sie einen Test schreiben, der den Baum abläuft und Ihnen eine 100% ige Abdeckung dieser Methode gibt, oder Sie können drei Einheitentests schreiben, die Ihnen auch eine 100% ige Abdeckung geben. Der Unterschied ist, dass Sie durch das Schreiben von drei Tests Kantenfälle testen, anstatt nur über den Baum zu laufen.


Warum sollten Sie überprüfen, ob Ihr DAL nur einmal angerufen wurde?
Marjan Venema

9

Hier ist eine gute Möglichkeit, über die Qualität Ihrer Software nachzudenken:

  1. Die Typprüfung behandelt einen Teil des Problems.
  2. Testen wird den Rest erledigen

Für Boilerplate- und Trivialfunktionen können Sie sich darauf verlassen, dass die Typprüfung ihre Aufgabe erfüllt, und für den Rest benötigen Sie Testfälle.


Natürlich funktioniert die Typprüfung nur, wenn Sie bestimmte Typen in Ihrem Code verwenden und entweder eine kompilierte Sprache verwenden oder auf andere Weise sicherstellen, dass eine statische Analyseprüfung häufig ausgeführt wird, z. B. als Teil von CI.
BDSL

6

Meiner Meinung nach ist die zyklomatische Komplexität ein Parameter. Wenn eine Methode nicht komplex genug ist (wie Getter und Setter). Es ist kein Komponententest erforderlich. McCabes zyklomatischer Komplexitätsgrad sollte mehr als 1 betragen. Ein anderes Wort sollte mindestens 1 Blockanweisung enthalten.


Denken Sie daran, dass einige Getter oder Setter Nebenwirkungen haben (obwohl dies in den meisten Fällen nicht empfohlen wird und als schlechte Praxis angesehen wird).
Andrzej Bobak

3

Ein klares JA mit TDD (und mit wenigen Ausnahmen)

In Ordnung umstritten, aber ich würde argumentieren, dass jedem, der diese Frage mit "Nein" beantwortet, ein grundlegendes Konzept von TDD fehlt.

Für mich ist die Antwort ein klares Ja, wenn Sie TDD folgen. Wenn nicht, dann ist nein eine plausible Antwort.

Die DDD in TDD

TDD wird oft als Hauptvorteil von dir angeführt.

  • Verteidigung
    • Sicherstellen, dass sich der Code möglicherweise ändert, aber nicht sein Verhalten .
    • Dies ermöglicht die sehr wichtige Praxis des Refactorings .
    • Sie erhalten diese TDD oder nicht.
  • Design
    • Sie legen fest, was etwas tun soll, wie es sich verhalten soll, bevor Sie es implementieren .
    • Dies bedeutet häufig fundiertere Umsetzungsentscheidungen .
  • Dokumentation
    • Die Testsuite sollte als Spezifikationsdokumentation (Anforderungsdokumentation) dienen.
    • Die Verwendung von Tests für diesen Zweck bedeutet, dass die Dokumentation und Implementierung immer in einem konsistenten Zustand sind - eine Änderung an einem bedeutet eine Änderung an einem anderen. Vergleichen Sie dies mit den Anforderungen und dem Design eines separaten Word-Dokuments.

Trennung von Verantwortung und Umsetzung

Als Programmierer ist es furchtbar verlockend, Attribute als etwas Bedeutungsvolles und Stärkeres und Setteres als eine Art Overhead zu betrachten.

Attribute sind jedoch ein Implementierungsdetail, während Setter und Getter die vertragliche Schnittstelle sind, über die Programme tatsächlich funktionieren.

Es ist viel wichtiger zu buchstabieren, dass ein Objekt:

Ermöglichen Sie seinen Clients, seinen Status zu ändern

und

Ermöglichen Sie seinen Clients, seinen Status abzufragen

dann, wie dieser Zustand tatsächlich gespeichert wird (für die ein Attribut die häufigste, aber nicht die einzige Möglichkeit ist).

Ein Test wie

(The Painter class) should store the provided colour

ist wichtig für den Dokumentationsteil von TDD.

Die Tatsache, dass die eventuelle Implementierung trivial ist (Attribut) und keinen Verteidigungsvorteil mit sich bringt , sollte Ihnen beim Schreiben des Tests nicht bekannt sein.

Das Fehlen von Round-Trip-Engineering ...

Eines der Hauptprobleme in der Welt der Systementwicklung ist das Fehlen von Round-Trip-Engineering 1 - der Entwicklungsprozess eines Systems ist in disjunkte Unterprozesse fragmentiert, deren Artefakte (Dokumentation, Code) häufig inkonsistent sind.

1 Brodie, Michael L. "John Mylopoulos: Samen der konzeptuellen Modellierung nähen." Konzeptuelle Modellierung: Grundlagen und Anwendungen. Springer Berlin Heidelberg, 2009. 1-9.

... und wie TDD das löst

Es ist der Dokumentationsteil von TDD, der sicherstellt, dass die Spezifikationen des Systems und sein Code immer konsistent sind.

Zuerst entwerfen, später implementieren

Innerhalb von TDD schreiben wir zuerst einen Test, bei dem die Abnahme fehlgeschlagen ist, und schreiben dann den Code, der sie passieren lässt.

Innerhalb des übergeordneten BDD schreiben wir zuerst Szenarien und lassen sie dann passieren.

Warum sollten Sie Setter und Getter ausschließen?

Theoretisch ist es innerhalb von TDD durchaus möglich, dass eine Person den Test schreibt und eine andere Person den Code implementiert, der ihn bestehen lässt.

Also frag dich:

Sollte die Person, die die Tests für eine Klasse schreibt, Getter und Setter erwähnen?

Da Getter und Setter eine öffentliche Schnittstelle zu einer Klasse sind, lautet die Antwort offensichtlich Ja , oder es gibt keine Möglichkeit, den Status eines Objekts festzulegen oder abzufragen.

Wenn Sie den Code zuerst schreiben, ist die Antwort möglicherweise nicht so eindeutig.

Ausnahmen

Es gibt einige offensichtliche Ausnahmen von dieser Regel - Funktionen, die klare Implementierungsdetails enthalten und eindeutig nicht Teil des Systemdesigns sind.

Zum Beispiel die lokale Methode 'B ()':

function A() {

    // B() will be called here    

    function B() {
        ...
    }
} 

Oder die private Funktion square()hier:

class Something {
private:
    square() {...}
public:
    addAndSquare() {...}
    substractAndSquare() {...}
}

Oder eine andere Funktion, die nicht Teil einer publicSchnittstelle ist, die im Entwurf der Systemkomponente geschrieben werden muss.


1

Wenn Sie mit einer philosophischen Frage konfrontiert werden, kehren Sie zu den fahrerischen Anforderungen zurück.

Ist es Ihr Ziel, einigermaßen zuverlässige Software zu wettbewerbsfähigen Kosten herzustellen?

Oder ist es so, dass Software fast unabhängig von den Kosten mit der höchstmöglichen Zuverlässigkeit hergestellt wird?

Bis zu einem gewissen Punkt stimmen die beiden Ziele Qualität und Entwicklungsgeschwindigkeit / -kosten überein: Sie verbringen weniger Zeit mit dem Schreiben von Tests als mit dem Beheben von Fehlern.

Aber darüber hinaus tun sie es nicht. Es ist nicht so schwer, einen gemeldeten Fehler pro Entwickler und Monat zu finden. Wenn Sie das auf einen von zwei Monaten halbieren, wird nur ein Budget von vielleicht ein oder zwei Tagen freigesetzt, und so viele zusätzliche Tests werden Ihre Fehlerquote wahrscheinlich nicht halbieren. Es ist also kein einfacher Gewinn mehr. Sie müssen dies anhand der Fehlerkosten für den Kunden begründen.

Diese Kosten variieren (und, wenn Sie böse sein möchten, auch ihre Fähigkeit, diese Kosten auf dem Markt oder in einem Rechtsstreit für Sie geltend zu machen). Du willst nicht böse sein, also zählst du diese Kosten vollständig zurück; manchmal machen einige Tests die Welt durch ihre Existenz immer noch global ärmer.

Kurz gesagt, wenn Sie versuchen, die gleichen Standards blind auf eine interne Website wie Passagierflugzeug-Flugsoftware anzuwenden, werden Sie entweder arbeitslos oder im Gefängnis sitzen.


0

Ihre Antwort darauf hängt von Ihrer Philosophie ab (glauben Sie, dass es Chicago gegen London ist? Ich bin sicher, dass jemand es nachschlagen wird). Die Jury ist immer noch nicht über den zeiteffektivsten Ansatz informiert (schließlich ist dies der größte Treiber dieser Zeit, die weniger für Korrekturen aufgewendet wurde).

Einige Ansätze sagen, dass nur die öffentliche Schnittstelle getestet wird, andere sagen, dass die Reihenfolge jedes Funktionsaufrufs in jeder Funktion getestet wird. Viele heilige Kriege wurden geführt. Mein Rat ist, beide Ansätze auszuprobieren. Wählen Sie eine Codeeinheit und machen Sie es wie X und eine andere wie Y. Gehen Sie nach ein paar Monaten Test und Integration zurück und sehen Sie, welche besser zu Ihren Anforderungen passt.


0

Das ist eine knifflige Frage.

Genau genommen würde ich sagen, dass es nicht notwendig ist. Es ist besser, Tests auf BDD-Einheits- und Systemebene zu schreiben, die sicherstellen, dass die Geschäftsanforderungen in positiven und negativen Szenarien wie beabsichtigt funktionieren.

Das heißt, wenn Ihre Methode von diesen Testfällen nicht abgedeckt wird, müssen Sie sich fragen, warum sie überhaupt existiert und ob sie benötigt wird oder ob versteckte Anforderungen im Code vorhanden sind, die sich nicht in Ihrer Dokumentation oder in User Stories widerspiegeln sollte in einem BDD-Testfall codiert werden.

Persönlich möchte ich die Zeilenabdeckung bei etwa 85-95% halten und das Einchecken in die Hauptzeile, um sicherzustellen, dass die vorhandene Testabdeckung pro Zeile für alle Codedateien auf diesem Niveau liegt und keine Dateien aufgedeckt werden.

Unter der Annahme, dass die besten Testmethoden befolgt werden, ergibt sich eine ausreichende Abdeckung, ohne dass Entwickler Zeit verlieren müssen, um herauszufinden, wie eine zusätzliche Abdeckung für schwer zu übenden Code oder Trivialcode einfach aus Gründen der Abdeckung erzielt werden kann.


-1

Das Problem ist die Frage selbst. Sie müssen nicht alle "Methoden" oder "Klassen" testen, um alle Funktionen Ihres Systems zu testen.

Es ist das zentrale Denken in Bezug auf Merkmale / Verhalten, anstatt in Bezug auf Methoden und Klassen zu denken. Natürlich gibt es hier eine Methode, die Unterstützung für eine oder mehrere Funktionen bietet. Am Ende wird der gesamte Code getestet, zumindest der gesamte Code in Ihrer Codebasis.

In Ihrem Szenario ist diese "Manager" -Klasse möglicherweise überflüssig oder unnötig (wie alle Klassen mit einem Namen, der das Wort "Manager" enthält), oder möglicherweise nicht, scheint jedoch ein Implementierungsdetail zu sein. Wahrscheinlich verdient diese Klasse keine Einheit Test, weil diese Klasse keine relevante Geschäftslogik hat. Möglicherweise benötigen Sie diese Klasse, damit ein Feature funktioniert. Der Test für dieses Feature deckt diese Klasse ab. Auf diese Weise können Sie diese Klasse umgestalten und Tests durchführen lassen, die sicherstellen, dass das, was zählt, Ihre Features auch nach dem Umgestalten noch funktionieren.

Denken Sie an Features / Verhaltensweisen, die nicht in Methodenklassen vorkommen. Ich kann dies nicht oft genug wiederholen.


-4

Das brachte mich jedoch zum Nachdenken. Sollten wir uns um die höchste Testabdeckung bemühen?

Ja, im Idealfall 100%, aber einige Dinge sind nicht prüfbar.

Getter und Setter (es sei denn, sie haben tatsächlich eine Logik in sich)

Getter / Setter sind dumm - benutze sie einfach nicht. Stellen Sie stattdessen Ihre Mitgliedsvariable in den öffentlichen Bereich.

Code "Boilerplate"

Holen Sie sich gemeinsamen Code heraus und testen Sie ihn. Das sollte so einfach sein.

Gibt es vernünftige / nicht "brennbare" Gründe, warum man jede einzelne (oder so viele wie möglich) Codezeile testen sollte?

Andernfalls könnten Sie einige sehr offensichtliche Fehler übersehen. Unit-Tests sind wie ein sicheres Netz, um bestimmte Arten von Fehlern zu erkennen, und Sie sollten es so oft wie möglich verwenden.

Und das Letzte: Ich arbeite an einem Projekt, bei dem die Leute nicht ihre Zeit damit verschwenden wollten, Komponententests für einen "einfachen Code" zu schreiben, sondern später beschlossen, überhaupt nicht zu schreiben. Am Ende verwandelten sich Teile des Codes in eine große Schlammkugel .


Nun, lassen Sie uns eines klarstellen: Ich wollte nicht, dass ich keine TDD- / Schreibtests verwende. Ganz im Gegenteil. Ich weiß, dass Tests Fehler finden könnten, über die ich nicht nachgedacht habe, aber was gibt es hier zu testen? Ich denke einfach, dass eine solche Methode eine der "nicht prüfbaren" ist. Wie Péter Török (unter Berufung auf Kent Beck) sagte, sollten Sie Dinge testen, die kaputt gehen können. Was könnte hier eventuell kaputt gehen? Nicht wirklich viel (bei dieser Methode gibt es nur eine einfache Delegierung). Ich KANN einen Komponententest schreiben, aber es wird einfach ein Mock des DAO und eine Behauptung haben, nicht viel testen. Für Getter / Setter sind sie für einige Frameworks erforderlich.
Zenzen

1
Außerdem, da ich es nicht bemerkte "Holen Sie sich gemeinsamen Code und Unit-Test. Das sollte so einfach sein." Was meinst du damit? Es handelt sich um eine Serviceklasse (in einer Serviceebene zwischen GUI und DAO), die für die gesamte App gilt. Kann es nicht wirklich allgemeiner machen (da es einige Parameter akzeptiert und eine bestimmte Methode im DAO aufruft). Der einzige Grund dafür ist, dass die Schichtenarchitektur der Anwendung beibehalten wird, sodass die GUI das DAO nicht direkt aufruft.
Zenzen

20
-1 für "Getter / Setter sind dumm - verwenden Sie sie einfach nicht. Setzen Sie stattdessen Ihre Mitgliedsvariable in den öffentlichen Bereich." - Sehr falsch. Dies wurde mehrmals auf SO diskutiert . Überall öffentliche Felder zu benutzen ist sogar schlimmer als überall Getter und Setter.
Péter Török
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.