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

Shopware E2E Testing with Playwright and ATS: A Practical Guide

Testing is a vital part of the software development process. It helps to detect bugs early, ensures functionality matches the requirements, and increases confidence in rolling out new features and updates. Of course, setting up the tests and writing them is an additional effort, but the time saved by this significantly outweighs the cost of writing tests. Tests in software development are a great investment.

We distinguish three main types of tests in software development:

  • Unit tests
  • Integration tests
  • End-to-end (E2E) tests

You can read more about them and the “Testing Pyramid” in this blog post. Here we will focus on the top level of that pyramid: End-to-end tests, specifically in the context of Shopware 6.

E2E tests in Shopware 6

In theory, you could pick up an E2E testing framework of your choice and start implementing the tests for your Shopware 6 shop. But writing E2E tests is not just about simulating user clicks and asserting the results. To ensure the tests are reliable and deterministic, you have to provide a reproducible environment for each test. For example, to test a product detail page, you should first create a test product. For that, you will write a Shopware Admin API client, implement functions for creating entities, provide data fixtures for those entities… A lot of work, and you didn’t even start writing the tests.

That’s why it’s generally better to stick with the preferred Shopware end-to-end testing framework, which is Playwright. Shopware provides the Acceptance Test Suite package (I will refer to it as ATS in this blog post), which is a Playwright extension. It solves the issues mentioned above and offers even more features to make Showpare E2E testing easier:

  • Ready to use data fixtures,
  • Test Data Service to create all sorts of Shopware entities: Products, Customers, Orders, etc.
  • Page Objects for the Storefront and Administration,
  • Actor Pattern.

Let’s discover it with some real-world examples.

How to Install the Shopware Acceptance Test Suite

The setup is very simple. All you need to do is follow the steps described in the documentation of Acceptance Test Suite.

First, install all necessary dependencies:

# Create a new Playwright project
npm init playwright@latest

# Install the ATS package:
npm install @shopware-ag/acceptance-test-suite

# Install Playwright with all dependencies 
npm install
npx playwright install
npx playwright install-deps

Then, create a .env file and playwright.config.ts as described in the documentation. The config will depend on the project and your needs, but playwrigt.config.ts from ATS repository should be a good base to start with.

Finally, create a BaseTestFile.ts to export everything from the acceptance-test-suite framework.

// 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>({
    // Your fixtures
});

Running and Extending ATS Fixtures in Playwright

When you are starting with Playwright or E2E tests in general, it’s a good idea to look into the tests implemented by Shopware. You can find them in their repository, in the tests/acceptance/tests directory.

Let’s start with something simple, like customer registration. Go to Account/CustomerRegistration.spec.ts and copy the first test case. Remember to import our custom test config from the BaseTestFile.ts.

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();
});

Now run the test with npx playwright test.

Note

It might or might not pass, depending on the shop you are testing, the configuration of the sales channel, language, the level of template customization, etc. For this blog post, I’m running the tests against one of the real-world projects that we maintain. I will use it as an example to show you what issues you can run into, and how you can extend the Acceptance Test Suite to match your shop and your requirements. The specific examples might be different from your project, but the general way of resolving issues and extending the ATS package should remain the same.

When I ran the test, a couple of things caused it to fail. Let’s take a closer look and see how each of them can be fixed.

Selecting the theme

If you ran the test, you probably noticed that it was executed on a default Shopware Storefront theme.

Playwright test runner showing Before Hooks fixtures list including Theme, SalesChannelBaseConfig, and DefaultSalesChannel. It is visible that the default Shopware theme was used to run the test.

This is probably not what you want, as the majority of the shops use a custom theme. ATS comes with a lot of predefined fixtures; one of them is responsible for returning the theme. The best way to find the fixture that you need to overwrite is to look at the list of fixtures executed in Before Hooks part of the Playwright test suite.

Screenshor of the Playwright test runner. "Before hooks" section is selected, showing all fixtures used in the test.

The next step is to find this fixture in the ATS source code – now we know what we need to overwrite.

First, let’s add THEME_ID environment variable to our .env file:

THEME_ID='092869698a3a471698cb4e0ae711d098'

Next, go to your BaseTestFile.ts and add this code:

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

All we do here is return the THEME_ID environment as Theme.id. Theme fixture is used inside SalesChannelBaseConfig fixture, so that’s all we need to do. If you re-run the test, it will create a test sales channel with your custom theme selected.

Using your existing sales channel for the tests

Another thing you might have noticed after running the test is the weird-looking URL, something like: http://localhost:8000/test-96f3eac9a011202680122131b52956a6/.

By default, ATS creates a new sales channel with default options for the tests. The test-96f3eac9a011202680122131b52956a6 part of the URL is just a domain of that new sales channel. There are some scenarios where running the tests on a fresh sales channel makes sense: testing core Shopware functionalities or testing a marketplace plugin that should be independent from the Sales Channel settings. However, the usual scenario is that the Sales Channel of your shop comes with a lot of configuration, and that configuration influences how the shop works, so it’s probably a good idea to include that in the tests.

As with all the fixtures that we want to modify, we need to find the DefaultSalesChannel in the source code of ATS first. Copy the fixture definition to your BaseTestFile.ts, same as we did with the Theme before. There are three places in the original implementation that we want to change.

Update the baseUrl accordingly, in most cases just remove the test-${uuid}/ part. This should reflect the base URL of your sales channel.

const baseUrl = `${SalesChannelBaseConfig.appUrl}`

Remove the block responsible for creating the default sales channel. We will be using an existing sales channel, so we don’t need it.

// Remove this block
const syncResp = await AdminApiContext.post('./_action/sync', {
...
})

Fetch the existing sales channel instead of the one with a random UUID:

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

Now, when you run any test that uses the DefaultSalesChannel fixture, it will use your existing sales channel instead of creating a new one with a base config.

Extending the page object

As mentioned earlier, ATS comes with “Page objects”. These are simple classes representing the HTML elements on Shopware pages. Instead of querying the email field in every registration-related test with something like page.getByLabel('Email'), you can simply refer to the StorefrontAccountLogin page object fixture: StorefrontAccountLogin.emailInput. This helps to avoid duplication and ensures consistency between tests.

That’s great, but the page objects provided in ATS only cover the default Shopware implementation. In most shops, the default templates will be modified via the theme, either by modifying existing elements or adding new ones. In my case, the problem was that we used a password confirmation input next to the standard password input. So, how do we extend the page objects by adding our custom elements?

We cannot really extend the page-object classes from ATS, as they are not publicly exported by the package. The best way is to create page-objects/storefront/AccountLogin.ts file in your playwright directory, paste the contents of the AccountLogin.ts there, and apply necessary changes. In my case, I had to modify the registerPasswordInput locator and add a new one: 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 *`)
       ...
   }
   ...
}
Tip

You can also overwrite methods of page-object classes. A common use case is overwriting the url() method that defines the URL of a given entity. E.g., if your product detail page uses a different URL pattern than the default one, just overwrite the ProductDetail page object the same way as we did with AccountLogin above.

Overwriting the test customer data

If you look into the source code of the Register action in ATS, you will notice that it comes with a predefined defaultRegistrationData object. Some of it doesn’t work in our case, because our shop is in German – e.g. we don’t have a salutation “Mr.”. As you might have noticed, this action also allows you to pass partial overrides, making it an easy fix:

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

Understanding the Actor Pattern in Shopware ATS

One of the concepts introduced by the Acceptance Test Suite is the Actor pattern, which is another layer of abstraction on top of Playwright and the page objects mentioned before. It consists of two additional entities:

  • Actor – e.g., ShopCustomer
  • Task – e.g., AddProductToCart, Register
await ShopCustomer.goesTo(StorefrontAccountLogin.url());
await ShopCustomer.attemptsTo(Register(customer));
await ShopCustomer.expects(StorefrontAccount.page.getByText(customer.email, { exact: true })).toBeVisible();

Another example could be a simple “add to cart” test scenario:

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

When I first saw this pattern, it reminded me of the Gherkin syntax, which we implemented in some of our Cypress E2E tests, using a preprocessor. Being able to move the acceptance criteria 1:1 into E2E test scenarios was nice, although it required some additional effort, as you need to translate each English sentence into a set of test commands. The Actor pattern suggested by ATS is a very good middle ground between Gherkin and a series of meaningless test commands. It definitely makes the test scenarios very clear and easy to understand, even for non-technical people.

Extending a task

Same as with extending page objects, sometimes we might want to extend or add a new Task. Let’s add a new AddVariantProductToCart task. We can base it on the AddProductToCart task. The only missing piece is the variant selection. Add the following code to your BaseTestFile.ts:

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);
},

There is one line that makes it different from the standard AddProductToCart task:

await ShopCustomer.selectsRadioButton(
   StorefrontProductDetail.variantSelect,
   variantLabel
)

Variants on our PDP are selectable as radio buttons, so we use the selectRadioButton method of the Actor. StorefrontProductDetail.variantSelect is a custom selector that we’ve added to the StorefrontProductDetail page object, the same way as we did for the password input field in the example from the previous section.

Finally, you can use the new task of a ShopCustomer actor in a test:

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')
})

Running Shopware E2E tests in CI/CD

Alright, we have some E2E tests implemented now. The next big question is, how do we run them in the CI/CD pipeline? We can distinguish between three different approaches, each with its pros and cons.

Running E2E tests against a fresh Shopware instance

Often, the first idea is to perform the tests against a fresh Shopware instance, created for every E2E test run in the pipeline. In theory, it sounds good: every test run starts from the same state, and we can create the data we need via fixtures.

There is one big problem with this approach. Most stores rely, to some extent, on Shopware configuration: advanced pricing, custom shipping rules, flows, etc. These settings often alter basic shop functionalities: rules can change product prices, hide certain payment methods in the checkout when conditions are met, and so on. Recreating these configurations as fixtures is, in most cases, simply not feasible. There can be hundreds of rules; they can change from week to week. Maintaining such fixtures to match the production environment seems impossible or extremely expensive and time-consuming at best.

My point is, if we run E2E tests on a fresh Shopware instance, and we cannot ensure that all the shop configuration matches the production environment, can we actually trust such tests?

There is one good use case for this approach, though, that I already mentioned earlier. That would be when you develop and want to test a standalone Shopware plugin – in that case, testing it in a fresh, default Shopware environment makes perfect sense.

Running E2E tests on a staging environment

Another idea is to perform the E2E tests against a live staging environment. This eliminates the problems of the first approach – staging environments should be as close as possible to production, so all configurations should be there too.

In this scenario, all we need to focus on is creating the necessary data fixtures (e.g., Products, Customers, Orders) before a test run and ensuring the state is properly cleaned up afterward. The main downside of running E2E tests on a staging environment is the potential for flakiness, as other actions, like manual testing, might be interacting with and modifying the environment simultaneously, leading to inconsistent test results.

In order for this approach to be reliable, frequent synchronization of staging with the sanitized production database is required. This ensures that the configurations are up-to-date and minimizes the risk of flakiness.

Baseline seed strategy

Another idea, that might be the best of both worlds, is to always initialize a new Shopware instance for the E2E tests, but use a sanitized production database snapshot as the starting point. This way, we don’t need to worry about recreating the configuration, and we also run the tests in an isolated environment.

The general idea would be something like this:

  1. A scheduled database export job produces sanitized production database dumps regularly.
  2. E2E pipeline starts a fresh Shopware instance and imports the latest dump.
  3. E2E tests create necessary data fixtures for Products, Customers, etc., the same way as they would when running on staging.

While this is probably the best approach, it requires some additional effort to set up. On the other hand, a production database export is usually needed regardless of the E2E tests, in order to sync pre-production environments.

Feature Fresh Shopware Instance Staging Environment Baseline Seed Strategy
Reliability Low: Tests may pass on a default shop but fail on the real configured shop. Medium: High realism, but prone to environment "drift" and interference. High: Most reliable as it mirrors production logic in total isolation.
Pros Guaranteed clean state Realistic configuration Best of both worlds
Cons Doesn't mirror production Manual testers can interfere Harder initial setup
Best For Standalone Plugins/Apps General maintenance of existing shops with frequent DB updates. Complex projects where configuration changes often.

Summary

I hope that this blog post helped you to get familiar with the new Shopware Acceptance Test Suite and understand how you can use it to speed up E2E tests implementation for a Shopware project.

I consider ATS to be a very good replacement for the old, no longer maintained, e2e-testsuite-platform, which was based on Cypress. Acceptance Test Suite offers much more flexibility while also reducing all that boilerplate that comes with creating basic data fixtures. Also, Playwright itself seems to be much more developer-friendly and has gained a lot of traction in recent years.

Chart showing the downloads of cypress and playwright npm packages over the last 2 years.
Downloads of cypress and playwright in the past 2 years: https://npmtrends.com/cypress-vs-playwright

E2E tests are usually the most time-consuming and require the most effort to maintain. Yet, they provide an additional level of safety that cannot be achieved by other test types – they automate the same user flows that are normally validated through manual QA.

Tired of manual testing before every release? We set up automated tests for Shopware shops so you can deploy with confidence. Find out how we work → Shopware Development & Consulting.

FacebookTwitterPinterest

Krzysiek Kaszanek

Senior Full Stack Engineer