Blog post image showing Krzysiek, a Build-Test-Deploy pipeline, and Shopware and Playwright logos.

Shopware-E2E-Tests mit Playwright und ATS: Ein praktischer Leitfaden

Software-Testing ist weit mehr als nur eine Pflichtübung in der Entwicklung. Es dient nicht nur der frühzeitigen Fehlererkennung, sondern garantiert auch, dass die Funktionalität exakt den definierten Anforderungen entspricht. Vor allem beim Rollout neuer Features oder Updates gibt es dir die nötige Sicherheit. Natürlich erfordert das Schreiben und Einrichten von Tests initialen Aufwand, doch die langfristige Zeitersparnis und Stabilität überwiegen die Kosten bei Weitem. Tests sind eine Investition, die sich in der Softwareentwicklung immer auszahlt.

In der Praxis unterscheiden wir im Wesentlichen drei Test-Kategorien:

  • Unit tests
  • Integrationstests
  • End-to-End-Tests (E2E)

Detaillierte Informationen hierzu und zur sogenannten "Testpyramide" findest du in diesem Blogbeitrag. In diesem Artikel widmen wir uns der Spitze dieser Pyramide: den End-to-End-Tests, speziell im Ökosystem von Shopware 6.

E2E-Tests in Shopware 6

Natürlich steht es dir frei, ein beliebiges E2E-Framework zu wählen und direkt Tests für deinen Shopware-Shop zu schreiben. Doch E2E-Testing ist weit komplexer als das bloße Simulieren von Klicks und das Abgleichen von Ergebnissen. Damit deine Tests verlässlich und deterministisch laufen, benötigst du für jeden Durchlauf eine saubere, reproduzierbare Umgebung. Ein klassisches Szenario: Du möchtest eine Produktdetailseite testen. Dafür muss zwingend vorher ein Testprodukt existieren. Du müsstest also zunächst einen Client für die Shopware Admin API schreiben, Funktionen zur Erstellung von Entities implementieren und Data Fixtures bereitstellen. Das ist ein enormer Aufwand, noch bevor du überhaupt die erste Zeile des eigentlichen Tests geschrieben hast.

Aus diesem Grund ist es meist der effizientere Weg, auf den Shopware-Standard zu setzen: Playwright. Shopware stellt hierfür das Acceptance Test Suite Paket bereit (im Folgenden kurz ATS genannt), das als leistungsstarke Erweiterung für Playwright fungiert. Es löst genau die oben genannten Hürden und bietet Features, die das E2E-Testing in Shopware massiv vereinfachen:

  • Sofort einsatzbereite Data Fixtures,
  • Einen Test Data Service zum Erstellen diverser Shopware-Entities: > Produkte, Kunden, Bestellungen usw.
  • Vorgefertigte Page Objects für die Storefront und die Administration,
  • Das etablierte Actor Pattern.

Schauen wir uns die Anwendung anhand von Praxisbeispielen genauer an.

So installieren Sie die Shopware Acceptance Test Suite

Das Setup ist denkbar unkompliziert. Folge einfach den Schritten in der Dokumentation der Acceptance Test Suite.

First, install all necessary dependencies:

# Ein neues Playwright-Projekt erstellen
npm init playwright@latest

# Das ATS-Paket installieren
npm install @shopware-ag/acceptance-test-suite

# Playwright mit allen Abhängigkeiten installieren
npm install
npx playwright install
npx playwright install-deps

Erstellen Sie anschließend eine .env-Datei und eine playwright.config.ts-Datei, wie in der Dokumentation beschrieben. Die Konfiguration hängt vom Projekt und Ihren Anforderungen ab, aber die Datei playwright.config.ts aus dem ATS-Repository bietet eine gute Grundlage.

Erstellen Sie abschließend eine BaseTestFile.ts-Datei, um alle Elemente des Akzeptanztest-Frameworks zu exportieren.

// BaseTestFile.ts
import { test as base } from '@shopware-ag/acceptance-test-suite';
import type { FixtureTypes } from '@shopware-ag/acceptance-test-suite';

export * from '@shopware-ag/acceptance-test-suite';

export const test = base.extend<FixtureTypes>({
    // Deine fixtures
});

Ausführen und Erweitern von ATS-Fixtures in Playwright

Gerade wenn du neu in der Welt von Playwright oder E2E-Tests bist, lohnt sich ein Blick in die Referenz-Implementierungen von Shopware. Du findest diese im Repository unter tests/acceptance/tests directory.

Beginnen wir mit einem klassischen Anwendungsfall: der Kundenregistrierung. Navigiere zu Account/CustomerRegistration.spec.ts und kopiere den ersten Testfall. Wichtig: Vergiss nicht, unsere eigene Test-Konfiguration aus der BaseTestFile.ts zu importieren.

import { test } from '../BaseTestFile'

test('As a new customer, I must be able to register in the Storefront.', { tag: ['@Registration', '@Storefront'] }, async ({
    ShopCustomer,
    StorefrontAccountLogin,
    StorefrontAccount,
    IdProvider,
    Register,
}) => {
    const customer = { email: IdProvider.getIdPair().uuid + '@test.com' };

    await ShopCustomer.goesTo(StorefrontAccountLogin.url());
    await ShopCustomer.attemptsTo(Register(customer));
    await ShopCustomer.expects(StorefrontAccount.page.getByText(customer.email, { exact: true })).toBeVisible();
});

Führe den Test anschließend mit npx playwright test aus.

Hinweis

Wundere dich nicht, falls der Test auf Anhieb fehlschlägt. Das ist völlig normal und hängt stark von der individuellen Konfiguration des Shops ab, den du testest – sei es der Sales Channel, die Spracheinstellungen oder tiefgreifende Template-Anpassungen. Für diesen Artikel lasse ich die Tests bewusst gegen ein echtes Kundenprojekt laufen. So sehen wir direkt, welche Hürden in der Praxis auftreten und wie wir die Acceptance Test Suite an spezifische Anforderungen anpassen. Deine Fehlermeldungen mögen im Detail anders aussehen, aber die generelle Vorgehensweise beim Debugging und der Erweiterung der ATS bleibt identisch.

Bei meinem Testlauf traten einige typische Stolpersteine auf, die zum Abbruch führten. Analysieren wir die Ursachen und Lösungen im Detail.

Das Theme auswählen

Beim Start des Tests wirst du vermutlich bemerkt haben, dass standardmäßig das Shopware-Storefront-Theme geladen wird.

Playwright-Testrunner zeigt die Liste der Before Hooks-Fixtures an, darunter Theme, SalesChannelBaseConfig und DefaultSalesChannel. Es ist ersichtlich, dass das Standard-Shopware-Theme zum Ausführen des Tests verwendet wurde.

Das dürfte kaum deinem tatsächlichen Anwendungsfall entsprechen, da die meisten Shops auf ein Custom Theme setzen. Die ATS bringt jedoch etliche vordefinierte Fixtures mit – darunter auch eine, die sich um die Bereitstellung des Themes kümmert. Welche Fixture du genau überschreiben musst, findest du am einfachsten heraus, indem du die Liste der Fixtures prüfst, die in den Before Hooks der Playwright Test Suite geladen werden.

Screenshot des Playwright Test-Runners. Der Abschnitt „Before hooks“ ist hervorgehoben und listet alle im Test verwendeten Fixtures auf.

Als Nächstes gilt es diese Fixture im ATS-Quellcode zu lokalisieren – so wissen wir exakt, wo wir ansetzen müssen.

Ergänzen wir zunächst die Umgebungsvariable THEME_ID in unserer .env-Datei:

THEME_ID='092869698a3a471698cb4e0ae711d098'

Navigiere anschließend in deine BaseTestFile.ts und füge folgenden Code ein:

export const test = base.extend<FixtureTypes>({
   ...
   Theme: [
       async ({}, use) => {
           const themeId = process.env['THEME_ID']
           await use({
               id: themeId,
           })
       },
       { scope: 'worker' },
   ],
   ...
})  

Dieser Schnipsel macht nichts weiter, als die THEME_ID aus der Umgebung an Theme.id zu übergeben. Da die Theme-Fixture innerhalb der SalesChannelBaseConfig-Fixture genutzt wird, genügt das vollauf. Startest du den Test nun erneut, wird ein Verkaufskanal erstellt, in dem dein eigenes Theme aktiv ist.

Den bestehenden Verkaufskanal für Tests nutzen

Womöglich ist dir nach dem Testlauf die etwas kryptische URL aufgefallen, etwa: http://localhost:8000/test-96f3eac9a011202680122131b52956a6/.

Standardmäßig fährt die ATS für Tests einen komplett neuen Sales Channel mit Standardeinstellungen hoch. Das Segment test-96f3eac9a011202680122131b52956a6 in der URL entspricht dabei schlicht der Domain dieses neuen Kanals. Es gibt durchaus Szenarien, in denen Tests auf einem frischen Sales Channel sinnvoll sind: etwa beim Prüfen von Core-Funktionalitäten oder eines Marktplatz-Plugins, das unabhängig von spezifischen Einstellungen funktionieren muss. Meistens bringt der Sales Channel deines Shops jedoch eine umfangreiche Konfiguration mit, die massiven Einfluss auf die Funktionsweise des Shops hat. Daher ist es fast immer ratsam, genau diese Konfiguration auch in die Tests einzubeziehen.

Wie bei allen Fixtures, die wir anpassen möchten, suchen wir zunächst den DefaultSalesChannel im ATS-Code. Kopiere die Definition in deine BaseTestFile.ts, nalog zu unserem Vorgehen beim Theme. Im ursprünglichen Code müssen wir an drei Stellen eingreifen:

Passe die baseUrl entsprechend an. Meist reicht es, den Teil test-${uuid}/ zu entfernen. Das Ergebnis sollte der Basis-URL deines Verkaufskanals entsprechen.

const baseUrl = `${SalesChannelBaseConfig.appUrl}`

Entferne den Block, der für die Erstellung des Default-Sales-Channels zuständig ist. Da wir auf einen existierenden Kanal zurückgreifen, ist dieser Schritt überflüssig.

// Diesen Block entfernen
const syncResp = await AdminApiContext.post('./_action/sync', {
...
})

Rufe den existierenden Sales Channel ab, anstatt einen neuen mit zufälliger UUID zu generieren.

const salesChannelId = process.env['SALES_CHANNEL_ID']
const salesChannelPromise = AdminApiContext.get(
   `./sales-channel/${salesChannelId}`
)

Startest du jetzt einen Test, der die DefaultSalesChannel-Fixture nutzt, läuft dieser gegen deinen echten Verkaufskanal anstatt gegen eine frisch generierte Instanz mit Basiskonfiguration.

Page Objects erweitern

Wie bereits angesprochen, liefert die ATS sogenannte „Page Objects“ mit. Dabei handelt es sich um schlanke Klassen, die HTML-Elemente auf Shopware-Seiten repräsentieren. Anstatt in jedem registrierungsbezogenen Test das E-Mail-Feld mühsam via page.getByLabel('Email') zu suchen, referenzierst du einfach die Fixture des StorefrontAccountLogin Page Objects: StorefrontAccountLogin.emailInput. Das vermeidet Redundanz und sorgt für konsistente Tests.

Das ist zwar praktisch, allerdings decken die ATS Page Objects lediglich den Shopware-Standard ab. In den meisten Shops werden die Standard-Templates durch das Theme angepasst – sei es durch Modifikation bestehender Elemente oder das Hinzufügen neuer. In meinem Fall bestand die Herausforderung darin, dass wir neben dem regulären Passwortfeld noch eine Eingabe zur Passwortbestätigung nutzen. Wie erweitern wir also die Page Objects um unsere eigenen Elemente?

Wir können die Page-Object-Klassen aus der ATS nicht direkt erweitern, da sie vom Paket nicht öffentlich exportiert werden. Der eleganteste Weg führt über eine neue Datei page-objects/storefront/AccountLogin.ts in deinem Playwright-Verzeichnis. Kopiere den Inhalt der originalen AccountLogin.ts dort hinein und nimm die nötigen Anpassungen vor. Ich musste beispielsweise den Locator für registerPasswordInput anpassen und einen neuen hinzufügen: registerPasswordConfirmationInput.

export class AccountLogin implements PageObject {
   ...
   public readonly registerPasswordInput: Locator
   public readonly registerPasswordConfirmationInput: Locator
   ...

   constructor(page: Page) {
       ...
       this.registerPasswordInput = this.personalFormArea.getByLabel(
           `${translate('storefront:login:register.password')} *`,
           { exact: true }
       )

       this.registerPasswordConfirmationInput =
           this.personalFormArea.getByLabel(`Passwort-Bestätigung *`)
       ...
   }
   ...
}
Tipp

Du kannst Methoden von Page-Object-Klassen auch überschreiben. Ein klassischer Anwendungsfall ist das Anpassen der url()-Methode, welche die URL einer bestimmten Entität definiert. Verwendet deine Produktdetailseite beispielsweise eine vom Standard abweichende URL-Struktur, überschreibst du einfach das ProductDetail Page Object, genau wie wir es oben beim AccountLogin getan haben.

Testkundendaten überschreiben

Ein Blick in den Quellcode der Register actionin der ATS verrät, dass sie ein vordefiniertes defaultRegistrationData-Objekt mitbringt. Das passt nicht immer – unser Shop ist beispielsweise auf Deutsch eingestellt und kennt die Anrede „Mr.“ nicht. Glücklicherweise erlaubt diese Action, wie du vielleicht bemerkt hast, auch das Übergeben partieller Overrides, was die Lösung denkbar einfach macht:

const customer = {
   email: IdProvider.getIdPair().uuid + '@test.com',
   salutation: 'Herr',
   country: 'Deutschland',
}

Das Actor-Pattern in Shopware ATS verstehen

Herzstück der Acceptance Test Suite ist das Actor pattern. Es fungiert als zusätzliche Abstraktionsschicht über Playwright und den zuvor erwähnten Page Objects und führt zwei neue Entitäten ein:

  • Actor –z. B. ShopCustomer
  • Task – z. B. AddProductToCart, Register

Wir haben das bereits beim Kundenregistrierungstest in Aktion gesehen:

await ShopCustomer.goesTo(StorefrontAccountLogin.url());
await ShopCustomer.attemptsTo(Register(customer));
await ShopCustomer.expects(StorefrontAccount.page.getByText(customer.email, { exact: true })).toBeVisible();

Ein weiteres Beispiel wäre ein simples „In den Warenkorb“-Szenario:

await ShopCustomer.goesTo(StorefrontProductDetail.url(ProductData))
await ShopCustomer.attemptsTo(AddProductToCart(ProductData))
await ShopCustomer.expects(
   StorefrontProductDetail.offCanvasSummaryTotalPrice
).toContainText('10,00')

Als ich dieses Pattern zum ersten Mal sah, erinnerte es mich stark an die Gherkin-Syntax, die wir in einigen unserer Cypress E2E-Tests mithilfe eines Preprocessor implementiert hatten. Die Möglichkeit, Akzeptanzkriterien 1:1 in E2E-Testszenarien zu überführen, war charmant, erforderte aber zusätzlichen Aufwand, da jeder englische Satz in eine Reihe von Testbefehlen übersetzt werden musste. Das von der ATS vorgeschlagene Actor Pattern ist ein gelungener Mittelweg zwischen Gherkin und einer Reihe nichtssagender Testbefehle. Es macht die Testszenarien definitiv sehr klar und verständlich, selbst für Nicht-Techniker.

Einen Task erweitern

Ähnlich wie bei Page Objects müssen wir manchmal einen Task erweitern oder einen neuen hinzufügen. Lass uns einen neuen AddVariantProductToCart-Task erstellen. Wir können ihn auf dem bestehenden AddProductToCart-Task aufbauen. Das einzige fehlende Puzzleteil ist die Variantenauswahl. Füge folgenden Code zu deiner BaseTestFile.ts hinzu:

AddVariantProductToCart: async ({ ShopCustomer, StorefrontProductDetail }, use) => {
   const task = (ProductData: FixtureTypes['ProductData'], variantLabel, quantity = "1") => {
       return async function AddProductToCart() {
           await ShopCustomer.selectsRadioButton(StorefrontProductDetail.variantSelect, variantLabel);

           await ShopCustomer.fillsIn(StorefrontProductDetail.quantitySelect, quantity);
           await ShopCustomer.presses(StorefrontProductDetail.addToCartButton);

           await ShopCustomer.expects(StorefrontProductDetail.offCanvasCartTitle).toBeVisible();
           await ShopCustomer.expects(StorefrontProductDetail.offCanvasCart.getByText(ProductData.name)).toBeVisible();
       };
   };

   await use(task);
},

Eine Zeile macht hier den entscheidenden Unterschied zum Standard-AddProductToCart-Task aus:

await ShopCustomer.selectsRadioButton(
   StorefrontProductDetail.variantSelect,
   variantLabel
)

Varianten sind auf unserer Detailseite als Radio-Buttons auswählbar, daher nutzen wir die selectRadioButton-Methode des Actors. StorefrontProductDetail.variantSelect ist dabei ein benutzerdefinierter Selektor, den wir dem StorefrontProductDetail Page Object hinzugefügt haben – genau wie wir es im Beispiel des vorherigen Abschnitts für das Passwortfeld getan haben.

Schließlich kannst du den neuen Task eines ShopCustomer-Actors in einem Test verwenden:

test('As a customer, I must be able to add a variant product to the cart', async ({
   ShopCustomer,
   StorefrontProductDetail,
   ProductData,
   AddVariantProductToCart,
}) => {
   await ShopCustomer.goesTo(StorefrontProductDetail.url(ProductData))
   await ShopCustomer.attemptsTo(AddVariantProductToCart(ProductData, 'XL', 2))
   await ShopCustomer.expects(
       StorefrontProductDetail.offCanvasSummaryTotalPrice
   ).toContainText('20,00')
})

Shopware E2E-Tests in der CI/CD-Pipeline

Soweit, so gut – die E2E-Tests stehen. Die nächste große Frage lautet: Wie integrieren wir sie in die CI/CD-Pipeline? Hier lassen sich drei verschiedene Ansätze unterscheiden, jeder mit seinen eigenen Vor- und Nachteilen.

E2E-Tests gegen eine frische Shopware-Instanz

Naheliegend ist oft der Gedanke, die Tests gegen eine frische Shopware-Instanz laufen zu lassen, die für jeden E2E-Testlauf in der Pipeline neu hochgefahren wird. In der Theorie klingt das vielversprechend: Jeder Testlauf startet im gleichen Zustand („Clean State“), und wir können die benötigten Daten über Fixtures erzeugen.

Dieser Ansatz birgt jedoch ein erhebliches Problem. Die meisten Shops verlassen sich stark auf ihre spezifische Shopware-Konfiguration: erweiterte Preisfindung, benutzerdefinierte Versandregeln, Flows usw. Diese Einstellungen verändern oft grundlegende Shop-Funktionalitäten: Regeln können Produktpreise ändern oder bestimmte Zahlungsarten im Checkout ausblenden, wenn Bedingungen erfüllt sind. Diese Konfigurationen als Fixtures nachzubauen, ist in der Praxis kaum machbar. Es kann Hunderte von Regeln geben, die sich zudem wöchentlich ändern können. Solche Fixtures synchron zur Produktionsumgebung zu halten, scheint unmöglich oder zumindest extrem aufwendig und teuer.

Mein Punkt ist: Wenn wir E2E-Tests auf einer frischen Shopware-Instanz ausführen und nicht garantieren können, dass die gesamte Shop-Konfiguration der Produktionsumgebung entspricht – wie aussagekräftig sind diese Tests dann wirklich?

Es gibt allerdings einen validen Anwendungsfall für diesen Ansatz, den ich bereits erwähnt habe: Wenn du ein eigenständiges Shopware-Plugin entwickelst, ergibt das Testen in einer frischen Standard-Shopware-Umgebung absolut Sinn.

E2E-Tests auf einer Staging-Umgebung

Eine Alternative ist, die E2E-Tests gegen eine live laufende Staging-Umgebung durchzuführen. Dies eliminiert die Probleme des ersten Ansatzes – Staging-Umgebungen sollten so nah wie möglich an der Produktion sein, folglich sind auch alle Konfigurationen vorhanden.

In diesem Szenario müssen wir uns lediglich darauf konzentrieren, die notwendigen Daten-Fixtures (z. B. Produkte, Kunden, Bestellungen) vor einem Testlauf zu erstellen und sicherzustellen, dass der Zustand danach ordnungsgemäß bereinigt wird. Der Hauptnachteil bei der Ausführung auf Staging ist die potenzielle Instabilität („Flakiness“), da andere Aktionen, wie manuelles Testen, gleichzeitig mit der Umgebung interagieren könnten. Dies kann zu inkonsistenten Testergebnissen führen.

Damit dieser Ansatz zuverlässig funktioniert, ist eine häufige Synchronisierung des Stagings mit der bereinigten Produktionsdatenbank unerlässlich. Dies stellt sicher, dass die Konfigurationen aktuell sind, und minimiert das Risiko von Instabilitäten.

Die Baseline-Seed-Strategie

Der Königsweg, der womöglich das Beste aus beiden Welten vereint, besteht darin, immer eine neue Shopware-Instanz für die E2E-Tests zu initialisieren, aber einen bereinigten Snapshot der Produktionsdatenbank als Startpunkt zu verwenden. Auf diese Weise müssen wir uns keine Sorgen um das Nachbauen der Konfiguration machen und führen die Tests gleichzeitig in einer isolierten Umgebung aus.

Die grundlegende Idee sieht etwa so aus:

  1. Ein geplanter Datenbank-Export-Job erstellt regelmäßig bereinigte Dumps der Produktionsdatenbank.
  2. Die E2E-Pipeline startet eine frische Shopware-Instanz und importiert den neuesten Dump.
  3. Die E2E-Tests erstellen die notwendigen Daten-Fixtures für Produkte, Kunden usw., genau wie sie es beim Ausführen auf Staging tun würden.

Obwohl dies wahrscheinlich der robusteste Ansatz ist, erfordert er initial einen höheren Einrichtungsaufwand. Andererseits wird ein Export der Produktionsdatenbank meist ohnehin benötigt, um Vorproduktionsumgebungen zu synchronisieren.

Feature Frische Shopware-Instanz Staging-Umgebung Baseline-Seed-Strategie
Zuverlässigkeit Niedrig: Tests laufen im Standard-Shop evtl. durch, scheitern aber im echt konfigurierten Shop. Mittel: Hoher Realismus, aber anfällig für Umgebungsabweichungen und Störungen. Hoch: Maximale Zuverlässigkeit, da Produktionslogik in totaler Isolation gespiegelt wird.
Vorteile Garantierter sauberer Zustand Realistische Konfiguration Das Beste aus beiden Welten
Nachteile Spiegelt nicht die Produktion wider Manuelle Tester können stören Komplexeres initiales Setup
Ideal für Standalone Plugins/Apps Allgemeine Wartung bestehender Shops mit häufigen DB-Updates. Komplexe Projekte, bei denen sich die Konfiguration oft ändert.

Fazit

Ich hoffe, dieser Blogbeitrag hat dir den Einstieg in die neue Shopware Acceptance Test Suite erleichtert und gezeigt, wie du sie nutzen kannst, um die Implementierung von E2E-Tests für dein Shopware-Projekt zu beschleunigen.

Meiner Meinung nach ist die ATS ein hervorragender Ersatz für die alte, nicht mehr gepflegte e2e-testsuite-platform, die noch auf Cypress basierte. Die Acceptance Test Suite bietet deutlich mehr Flexibilität und reduziert gleichzeitig den ganzen Boilerplate-Code, der üblicherweise mit der Erstellung grundlegender Daten-Fixtures einhergeht. Zudem erweist sich Playwright als deutlich entwicklerfreundlicher und hat in den letzten Jahren zurecht enorm an Fahrt aufgenommen.

Diagramm mit den Downloads der npm-Pakete Cypress und Playwright in den letzten 2 Jahren.
Downloads von „cypress“ und „playwright“ in den letzten 2 Jahren: https://npmtrends.com/cypress-vs-playwright

Auch wenn E2E-Tests meist am zeitintensivsten sind und den höchsten Wartungsaufwand mit sich bringen, schaffen sie ein Sicherheitsniveau, das mit anderen Testmethoden unerreichbar bleibt. Schließlich automatisieren sie genau jene User Flows, die andernfalls manuell durch die Qualitätssicherung validiert werden müssten.

Sind Sie es leid, vor jeder Veröffentlichung manuelle Tests durchzuführen? Wir richten automatisierte Tests für Shopware-Shops ein, damit Sie Ihre Produkte sicher bereitstellen können. Erfahren Sie mehr über unsere Arbeitsweise → Shopware-Entwicklung und -Beratung.

FacebookTwitterPinterest

Krzysiek Kaszanek

Senior Full Stack Engineer

Ich bin Student der Informatik und Praktikant bei Kiwee. Seit meinem ersten C++-Tutorial habe ich mich in die Programmierung verliebt. Ich liebe es, Probleme zu lösen und herauszufinden, wie Dinge funktionieren. Webentwicklung ist die Sache, die mir die größte Zufriedenheit in der Programmierung gibt, und mein Ziel ist es, ein Full-Stack-Developer zu werden. Nach einem anstrengenden Tag an der Universität gehe ich klettern oder Fahrrad fahren. Sport hilft mir, Stress abzubauen und meinen Geist neu zu beleben.