Ich hatte versprochen, dass wir uns in den folgenden Blogposts um weiterführende Themen kümmern werden. Diesmal wollen wir unsere SOAP-Services testen. Doch wie teste ich einen Webservice aus einem Unit-Test heraus? Wie baut man einen Integrationstest? Und gibt es da nicht auch noch etwas dazwischen? O.K., der Reihe nach.
Spring Boot & Apache CXF – Tutorial
Part 1: Spring Boot & Apache CXF – SOAP ohne XML?
Part 2: Spring Boot & Apache CXF – SOAP Webservices testen
Part 3: Spring Boot & Apache CXF – XML-Validierung und Custom SOAP Faults
Part 4: Spring Boot & Apache CXF – Logging & Monitoring mit Logback, Elasticsearch, Logstash & Kibana
Part 5: Spring Boot & Apache CXF – Von 0 auf SOAP mit dem cxf-spring-boot-starter
Im letzten Blogpost haben wir uns angeschaut, wie man Spring Boot und Apache CXF am besten „verdrahtet“. Dabei generieren wir die nötigen Java-Klassen aus WSDL und importieren XSDs elegant per JAX-WS-Maven-Plugin und vermeiden das Einchecken generierter Artefakte ins Versionskontrollsystem. So sind wir immer auf dem aktuellen Stand bzgl. der Schnittstellenbeschreibung („Contract first“). Daneben konfigurieren wir Apache CXF zu 100 % per Spring-Java-Konfiguration und wissen, wie man damit einen lauffähigen SOAP-Endpunkt bereitstellt.
Doch nun wollen wir endlich den (bisher stiefmütterlich behandelten) Source Folder src/test/ mit Leben füllen. Denn bisher haben wir keinerlei Tests geschrieben, obwohl diese doch auf keinen Fall fehlen sollten. Das erste Refactoring kommt bestimmt. Und gerade SOAP-Webservices können sehr komplex werden – da sind Tests unverzichtbar.
Unit-Tests (aka yxzTest.class)
Die folgenden Schritte kann man wie immer komplett im Github-Repository tutorial-soap-spring-boot-cxf nachvollziehen. Das passende Beispielprojekt step4_test findet sich dort auch.
Unsere Endpoint-Klasse , die wir vom generierten Service Endpoint Interface (SEI) abgeleitet haben, ist ein ganz normales POJO bzw. eine Spring-Komponente. Wir können sie also wie gewohnt mit dem new-Operator erzeugen und Unit-Tests nach Lust und Laune schreiben.
Da der Endpoint selbst aber keinen fachlichen Code enthalten sollte (weil er ja quasi durch Infrastruktur-Code „verschmutzt“ ist), delegiert er im Idealfall die Arbeit an eine andere Komponente – so etwas wie MyFancyServiceController. Nun ist es nur bedingt sinnvoll, unseren WebServiceEndpoint nach der reinen Lehre vollständig isoliert zu testen (wenngleich möglich) – meist wird man doch ein bisschen Spring hinzunehmen und etwas komplexere Abläufe testen wollen.
Hierzu erweitern wir unser Beispiel aus Step 3 um einen rudimentären „WeatherServiceController“ und konfigurieren diesen in einer separaten ApplicationConfiguration als Spring-Bean. Der WeatherServiceController liefert in seiner einzigen Methode getCityForecastByZIP(ForecastRequest forecastRequest) mithilfe eines ebenfalls hinzugekommenen GetCityForecastByZIPOutMapper eine Weather-Service-XSD-konforme Antwort zurück.
Aus unserem WeatherServiceEndpoint heraus greifen wir auf den injizierten WeatherServiceController zu – und schon haben wir lauffähigen Code, den wir testen können. Natürlich sollten wir dabei diese einfache Implementierung nur als Beispiel sehen, in dem viel fehlt: Inbound-Transformation, eventuelle Plausibilisierungsprüfungen, verschiedene Backend-Calls usw.
Unsere Testklasse WeatherServiceTest.java ist nun denkbar einfach realisiert. Uns genügen dabei die beiden Annotationen @RunWith(SpringJUnit4ClassRunner.class) sowie @ContextConfiguration(classes=ApplicationTestConfiguration.class), um unseren SpringApplication-Context zu initialisieren und die beiden für unseren Test nötigen Beans (WeatcherServiceEndpoint & WeatherServiceController) zu instanziieren (konfiguriert in der ApplicationTestConfiguration.java ).
In der mit @Test annotierten Methode erstellen wir uns einen Request und rufen die passende Methode unseres injizierten (@Autowired) Endpoints auf:
1@RunWith(SpringJUnit4ClassRunner.class)
2@ContextConfiguration(classes=ApplicationTestConfiguration.class)
3public class WeatherServiceTest {
4
5 @Autowired
6 private WeatherServiceEndpoint weatherServiceEndpoint;
7
8 @Test
9 public void getCityForecastByZIP() throws WeatherException {
10 // Given
11 ForecastRequest forecastRequest = generateDummyRequest();
12
13 // When
14 ForecastReturn forecastReturn = weatherServiceEndpoint.getCityForecastByZIP(forecastRequest);
15
16 // Then
17 assertNotNull(forecastReturn);
18 // many asserts here
19 assertEquals("22%", forecastReturn.getForecastResult().getForecast().get(0).getProbabilityOfPrecipiation().getDaytime());
20 }
21}
Fertig ist unser erster Test – ist alles grün, wissen wir, dass unser Endpoint-POJO schonmal das tut, was es soll.
Integrationstests (aka yxzIntegrationTest.class)
Soweit ist alles bekanntes Vorgehen zum Testen mit Spring. Nun wird es interessanter: Wie teste ich die SOAP-Services selbst?
Integrationstests sollten möglichst viele Komponenten während des Testlaufs mit einbeziehen. Da hier meist einige Backends angesprochen werden, summiert sich allerdings die Zeit schnell auf, die für die Ausführung mehrerer Integrationstests benötigt wird. Deshalb sollte man sie aus dem normalen Build heraushalten, z.B. mit dem Maven-Surefire-Plugin :
1<plugin> 2 <groupId>org.apache.maven.plugins</groupId> 3 <artifactId>maven-surefire-plugin</artifactId> 4 <configuration> 5 <excludes> 6 <exclude>**/*IntegrationTest.java</exclude> 7 </excludes> 8 </configuration> 9</plugin>
Nun werden diese Tests nicht während eines mvn install oder mvn package aufgerufen. Der Aufruf kann nun manuell in der IDE (oder im Hintergrund mit Plugins wie Infinitest ) und automatisch entkoppelt vom normalen Build-Job im CI-Server ablaufen. Dazu erstellt man z.B. ein Maven Profile, das die Integrationstest explizit wieder einschließt, und übergibt dieses Profil nur in dem Integrationstest-Job an das Maven CLI.
Doch jetzt zum Eigentlichen. Die Konfiguration unseres SOAP-Service im Client-Modus erfolgt mithilfe der.apache.cxf.jaxws.JaxWsProxyFactoryBean. Ihr übergeben wir unser Service Endpoint Interface (SEI) in der Methode setServiceClass() und konfigurieren die URL, unter der wir den Service z.B. auch per SoapUI aufrufen würden. Dazu ist es hilfreich, die für die CXFServlet verwendete Base-URL sowie den darauf folgenden Teil der Service-URL in unserer WebServiceConfiguration für den Test z.B. als Konstanten zugreifbar zu machen.
Zuletzt rufen wir die Methode .create() unserer konfigurierten JaxWsProxyFactoryBean-Instanz auf. Das Ergebnis ist unser JAX-WS-Service-Client, den wir noch auf unser SEI casten, damit auch alle notwendigen Methoden zur Verfügung stehen (warum das CXF-API hier noch ohne Generics arbeitet, bleibt ein Geheimnis der Apache-CXF-Entwickler). Die Configuration-Klasse für Integrationstests WebServiceIntegrationTestConfiguration.java sieht dann so aus:
1@Configuration
2public class WebServiceIntegrationTestConfiguration {
3
4 @Bean
5 public WeatherService weatherServiceIntegrationTestClient() {
6 JaxWsProxyFactoryBean jaxWsProxyFactory = new JaxWsProxyFactoryBean();
7 jaxWsProxyFactory.setServiceClass(WeatherService.class);
8 jaxWsProxyFactory.setAddress("http://localhost:8080" + WebServiceConfiguration.BASE_URL + WebServiceConfiguration.SERVICE_URL);
9 return (WeatherService) jaxWsProxyFactory.create();
10 }
11}
Unsere neue Testklasse WeatherServiceIntegrationTest weicht nur leicht von der für Unit-Tests ab. Wir konfigurieren unsere WebServiceIntegrationTestConfiguration und injizieren den JAX-WS-Service-Client statt des Endpoint-POJOs. Alles andere kann gleich bleiben:
1@RunWith(SpringJUnit4ClassRunner.class)
2@ContextConfiguration(classes=WebServiceIntegrationTestConfiguration.class)
3public class WeatherServiceIntegrationTest {
4
5 @Autowired
6 private WeatherService weatherServiceIntegrationTestClient;
7
8 @Test
9 public void getCityForecastByZIP() throws WeatherException {
10 // Given
11 ForecastRequest forecastRequest = generateDummyRequest();
12
13 // When
14 ForecastReturn forecastReturn = weatherServiceIntegrationTestClient.getCityForecastByZIP(forecastRequest);
15
16 // Then
17 assertNotNull(forecastReturn);
18 // many asserts here
19 assertEquals("22%", forecastReturn.getForecastResult().getForecast().get(0).getProbabilityOfPrecipiation().getDaytime());
20 }
21}
Führen wir den Test nun aus, schlägt er fehl (javax.xml.ws.WebServiceException: Could not send Message […] Caused by: java.net.ConnectException: Connection refused). Wir müssen natürlich noch unseren SOAP-Server starten – z.B. mithilfe eines „Run as…“ auf unserer SimpleBootCxfApplication.java . Als Integrationstest sollte unser Test schließlich die komplette SOAP-Kommunikation inkl. XML-Java-Marshalling und „Backendverarbeitung“ beinhalten. Haben wir unseren Server gestartet, läuft der anschließend ausgeführte Test auch anstandslos durch.
Und keine Angst wegen des manuellen Schrittes zum Starten des Servers: Haben wir unsere Pipeline inkl. der Stages aufgebaut, laufen die Integrationstests natürlich automatisch.
Single-System-Integrations-Tests (aka yxzSystemTest.class)
War das schon alles? In meinem aktuellen Projekt wurde schnell ersichtlich, dass die bekannte Einteilung in Unit- & Integrationstests nicht ausreicht. Denn mit den Unit-Tests prüfen wir ganz am Anfang des Entwicklungsprozesses, ob unsere POJOs das tun, was sie sollen. Die Integrationstests folgen erst ganz am Ende – als letzter (Jenkins-) Job in der Kette, wenn wirklich alles schon deployed ist. Doch alle für die Kommunikation notwendigen Komponenten sollten wir irgendwie schon mal dazwischen testen, sagt das Bauchgefühl. Späte Fehler (wie in unseren Integrationstests) sind bekanntlich teurer als die, die wir früher machen.
Aus diesem Gefühl heraus und natürlich mithilfe der Möglichkeiten von Spring (Boot) entstand die Idee für eine weitere Variante von Tests – Integrationstests, die aber komplett auf einem System ablaufen und alle notwendigen Komponenten zu ihrer Ausführungszeit entweder hoch- und wieder runterfahren – oder ganz ausmocken. Über Namen kann man bekanntlich streiten, aber wir haben diese einfach Single-System-Integrationstests (kurz SystemTest) getauft. Sie sind im Grunde die technisch interessanteste Variante von Testfällen für SOAP-Services. Warum das so ist, sehen wir gleich.
Eines vorweg: Diese Art der Integrationstests müssen und sollten wir nicht von unserem regulären Build ausschließen. Da sie auch nicht „IntegrationTest“ im Namen tragen, sollte der vorgeschlagene Ausschluss per Surefire-Plugin auch nicht „ziehen“.
Die Konfiguration eines solchen Tests läuft nahezu zu 100 % identisch zum normalen Integrationstest. Sie unterscheidet sich nur in dem Host und Port der Adresse – und das auch nur, wenn wir unsere Pipeline schon soweit aufgebaut haben, dass es einen Server gibt, auf den unser SOAP-Server deployed wird und gegen den die normalen Integrationstests laufen. In unserem Beispiel hier könnten wir den Unterschied sogar ignorieren – aber zu Demonstrationszwecken (und weil wir es sowieso brauchen werden), legen wir uns eine neue Configuration-Klasse WebServiceSystemTestConfiguration.java an und vergeben zumindest einen eigenen Port (8090). Zusätzlich benennen wir unsere Bean weatherServiceSystemTestClient() statt weatherServiceIntegrationTestClient(), damit Spring auch die richtige injizieren kann.
1jaxWsProxyFactory.setAddress("http://localhost:8090" + WebServiceConfiguration.BASE_URL + WebServiceConfiguration.SERVICE_URL);
Im Unterschied zum Integrationstest wollen wir unseren SOAP-Server innerhalb der Testausführung hochfahren. Dazu benötigen wir ein Klasse, die genauso wie unsere SimpleBootCxfApplication mit @SpringBootApplication annotiert ist, aber natürlich eine andere (Test-)Konfiguration lädt. Unsere neue SimpleBootCxfSystemTestApplication.java importiert dabei die WebServiceSystemTestConfiguration:
1@SpringBootApplication
2@Import(WebServiceSystemTestConfiguration.class)
3public class SimpleBootCxfSystemTestApplication {
4
5 public static void main(String[] args) {
6 SpringApplication.run(SimpleBootCxfSystemTestApplication.class, args);
7 }
8}
Nun zum eigentlichen Test. Die Klasse WeatherServiceSystemTest nutzt die gewohnte @RunWith-Annotation, verwendet aber statt @ContextConfiguration die Annotation @SpringApplicationConfiguration, der wir unsere SimpleBootCxfSystemTestApplication.class übergeben. Zusätzlich verwenden wir @WebIntegrationTest, welche die eigentliche „Magic“ ablaufen lässt – nämlich innerhalb des Tests unseren SOAP-Server hochzuziehen, diesen für alle Testmethoden wiederzuverwenden und zum Abschluss wieder herunterzufahren. Ihr übergeben wir ebenfalls unseren „SystemTest-Port“ 8090 – denn hierauf zielt ja unsere Konfiguration. Zuletzt benennen wir gegenüber dem Integrationstest noch unseren injizierten WeatherService „weatherServiceSystemTestClient“, sodass Spring korrekt autowired. Wieder bleibt der Rest des Test nahezu identisch zu unserem ursprünglichen Unit-Test:
1@RunWith(SpringJUnit4ClassRunner.class)
2@SpringApplicationConfiguration(classes=SimpleBootCxfSystemTestApplication.class)
3@WebIntegrationTest("server.port:8090")
4public class WeatherServiceSystemTest {
5
6 @Autowired
7 private WeatherService weatherServiceSystemTestClient;
8
9 @Test
10 public void getCityForecastByZIP() throws WeatherException {
11 // Given
12 ForecastRequest forecastRequest = generateDummyRequest();
13
14 // When
15 ForecastReturn forecastReturn = weatherServiceSystemTestClient.getCityForecastByZIP(forecastRequest);
16
17 // Then
18 assertNotNull(forecastReturn);
19 // many asserts here
20 assertEquals("22%", forecastReturn.getForecastResult().getForecast().get(0).getProbabilityOfPrecipiation().getDaytime());
21 }
22}
In unserem einfachen Beispiel hier wird die Mächtigkeit dieser Tests nicht unbedingt sofort sichtbar – einige Teamkollegen sind im Projekt auch schonmal mit dem Satz „Das kann ja nicht so schwer sein…“ an die Sache herangegangen – und waren dann doch überrascht, was hinter den Testfällen teilweise alles automatisch abläuft. Wenn erstmal ein richtig großer „Enterprise-SOAP-Endpoint“ (wie z.B. ein BiPro-Service) mal eben innerhalb einer Testklasse geladen wird und auf Herz und Nieren geprüft wird, dann findet man doch schnell Gefallen an der Sache. Denn ändert sich irgendwo ein noch so kleines Zahnrad im Getriebe, wird es schnell „rot“ in der IDE oder auf dem CI-Server – gute und sinnvolle Tests natürlich vorausgesetzt (wie man die schreibt, erklären meine Kollegen in vielen Blogbeiträgen, zuletzt erst wieder in Writing Better Tests With JUnit ).
Umgang mit Testfällen
Nachdem wir uns die unterschiedlichen Arten von Testfällen angesehen haben, sollten wir noch einen Aspekt andiskutieren: Egal, mit welcher Technologie man SOAP-Services anbietet – es sind letztlich immer XML-Anfragen, die mein Endpoint verarbeiten können muss. Für mich ist es dabei immer beruhigend zu wissen, dass die Anfragen, die jemand an meinen Service stellt (und die ich sehr gut mit SoapUI nachstellen kann), auch von meinen Services verarbeitet werden können. Dies führt dazu, dass man diese Anfragen auch in automatisierten Tests immer wieder ablaufen lassen können möchte.
Nun stellt sich sofort die Frage, wo denn diese Testfälle am besten gespeichert und vorgehalten werden, wie sie an alle Nutzer verteilt und wie sie versioniert werden können. Zusätzlich sollten sie auch möglichst immer wieder angepasst werden, sobald sich etwas ändert (z.B. ein XML-Schema), und es sollte nicht zu viele redundante Kopien geben, die wiederum gepflegt werden müssen. Rund um diese Fragen wurden schon millionenschwere, aber unbedienbare Tools und Repositories verkauft, wie ich selber in meiner Diplomarbeit vor vielen Jahren leidvoll erfahren durfte.
Warum also sollte man nicht einfach mal diese ganzen schwergewichtigen Tools weglassen und sich auf einen radikal einfachen Ansatz einlassen? Auch wenn dieser vielleicht nicht alle Anforderungen zu 100 % abdeckt, dafür aber zu immer aktuellen Testfällen führt und bei dem alle am Projekt beteiligten Entwickler sofort Alarm schlagen, weil ihre Entwicklungsumgebungen rote Testcases produzieren und Jenkins-Jobs nicht erfolgreich durchlaufen?
Der Ansatz ist so einfach wie praktisch: Wir legen alle Testfälle in Form von .xml-Dateien im Projektordner für Testressourcen ab – z.B. unter src/test/resources/requests – und laden diese in unsere immer größer werdende Anzahl von Unit-, Integrations- und System-Tests. Darin nutzen wir die Power der Java-XML-Marshaller. Das hat viele Vorteile und gibt uns die Möglichkeit, jeden Testfall 1:1 auch manuell per SoapUI gegen den Service laufen zu lassen, um stichprobenartig nochmals für ein besseres Gefühl zu sorgen oder einfach, um Fehler nachzustellen. Ein Testfall, den wir unter src/test/resources/requests als XYZ-Testcase.xml ablegen, sieht für unseren Beispiel-WebService etwa so aus:
1<?xml version="1.0" encoding="UTF-8"?> 2<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:gen="http://www.codecentric.de/namespace/weatherservice/general"> 3 <soapenv:Header/> 4 <soapenv:Body> 5 <gen:GetCityForecastByZIP> 6 <gen:ForecastRequest> 7 <gen:ZIP>99425</gen:ZIP> 8 <gen:flagcolor>bluewhite</gen:flagcolor> 9 <gen:productName>ForecastBasic</gen:productName> 10 <gen:ForecastCustomer> 11 <gen:Age>30</gen:Age> 12 <gen:Contribution>5000</gen:Contribution> 13 <gen:MethodOfPayment>Paypal</gen:MethodOfPayment> 14 </gen:ForecastCustomer> 15 </gen:ForecastRequest> 16 </gen:GetCityForecastByZIP> 17 </soapenv:Body> 18</soapenv:Envelope>
Der einzige Haken an der Sache: Die extrem einfache Konfiguration mit automatischem XML-to-Java-Marshalling der WebService-Frameworks kann ich hier nicht nutzen, ich muss JAX-B „zu Fuß“ bändigen. Aber die Hürde ist nicht allzu hoch: Entweder man entwickelt sich eine eigene Hilfsklasse, die solche wiederkehrenden Arbeiten übernimmt; oder man schaut sich die Klasse XmlUtils aus dem Beispielprojekt genauer an. Speziell die (zugegeben langatmig benannte 😉 ) Methode readSoapMessageFromStreamAndUnmarshallBody2Object(InputStream fileStream, Class jaxbClass) bietet schon alles, was wir dazu benötigen.
Sie parst mithilfe der im JDK mitgelieferten Parser den InputStream eines XML-Files und baut daraus ein org.w3c.dom.Document . Aus diesem sucht sie sich den für das JAX-B-Marshalling gewünschten Inhalt des SOAP-Bodys heraus und marshalled diesen in Form eines des übergebenen JAX-B-POJOs, das natürlich mithilfe des JAX-WS-Maven-Plugin generiert wurde (siehe Part 1 dieses Tutorials ).
Mit diesem Objekt haben wir unseren XML-Testfall genau in der Form, wie wir ihn in unseren Testcases benötigen. Das Ergebnis finden wir in der Klasse WeatherServiceXmlFileSystemTest.java , die sich nur in wenigen Zeilen von der des weiter oben gezeigten SystemTests unterscheidet:
1@RunWith(SpringJUnit4ClassRunner.class)
2@SpringApplicationConfiguration(classes=SimpleBootCxfSystemTestApplication.class)
3@WebIntegrationTest("server.port:8090")
4public class WeatherServiceXmlFileSystemTest {
5
6 @Autowired
7 private WeatherService weatherServiceSystemTestClient;
8
9 @Value(value="classpath:requests/GetCityForecastByZIPTest.xml")
10 private Resource getCityForecastByZIPTestXml;
11
12 @Test
13 public void getCityForecastByZIP() throws WeatherException, XmlUtilsException, IOException {
14 // Given
15 GetCityForecastByZIP getCityForecastByZIP = XmlUtils.readSoapMessageFromStreamAndUnmarshallBody2Object(getCityForecastByZIPTestXml.getInputStream(), GetCityForecastByZIP.class);
16
17 // When
18 ForecastReturn forecastReturn = weatherServiceSystemTestClient.getCityForecastByZIP(getCityForecastByZIP.getForecastRequest());
19
20 // Then
21 assertNotNull(forecastReturn);
22 // many asserts here
23 assertEquals("22%", forecastReturn.getForecastResult().getForecast().get(0).getProbabilityOfPrecipiation().getDaytime());
24 }
25}
Mit dem Einlesen der XML-Testfälle schlagen wir uns übrigens auch nicht selbst herum, sondern lagern das einfach an Springs org.springframework.core.io.Resource aus, der wir per @Value-Annotation sagen, woher sie den Testfall lesen soll – wie gesagt irgendwo unterhalb von src/test/resources/requests. Wichtig ist hier, das Schlüsselwort „classpath:“ voranzustellen, dann sollte nichts schiefgehen.
Nun geht es dem Entwickler-Herzen ein ganzes Stück besser – wir können unsere SOAP-Webservices vernünftig und automatisiert testen. Wartung, Fehlersuche und Refactoring werden wesentlich erleichtert – nur um ein paar der Vorteile zu nennen. Zusätzlich kann man durch die hier vorgestellten Testmethoden komplett auf schwergewichtige Tools verzichten und dokumentiert gleichzeitig innerhalb der Tests, wie man die Services konkret nutzt. Denn auch wenn einem der SOAP-Standard mit WSDL & XSDs mächtige Werkzeuge zur Validierung an die Hand gibt, lassen sie doch immer noch genug Möglichkeiten zur Interpretation.
Doch es bleiben noch offene Baustellen. Die Namespace-Prefixes der SOAP-Responses sehen teilweise noch fürchterlich aus („ns1“, „ns2“, …), und die 200 Seiten starke Spezifikation unseres Webservices fordert eine XML-Schema-konforme Antwort, selbst wenn jemand völlig sinnfreie Anfragen gegen unseren Endpoint schickt. Außerdem will der Betrieb wissen, ob unser Service (noch) verfügbar ist, und wir wollen sehen, welche Anfragen eigentlich so ins System laufen. Wie wir damit umgehen, klären wir in einem der nächsten Teile dieser Blogreihe.
Weitere Beiträge
von Jonas Hecht
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Jonas Hecht
Senior Solution Architect
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.