MFTF in Magento 2: The Functional Testing Framework From Scratch
AI generated
M2
di.xml
Magento 2 · MFTF · Functional Testing · QA
MFTF: the Magento Functional Testing Framework
built up from scratch

MFTF automates exactly the class of bugs that unit tests systematically miss: broken checkout flows, faulty JavaScript interactions and admin forms that suddenly stop saving after an update. Whoever sets up MFTF properly gets a browser driven safety net that replays real click paths in storefront and backend automatically and catches regressions before customers do.

17 min read Test XML · ActionGroups · Page Objects · CI Magento 2.4.8 · PHP 8.4

1. What MFTF is and which problem it solves

MFTF, the Magento Functional Testing Framework, is Adobe's official tool for automated end to end tests in Magento 2. Unlike PHPUnit tests, which check individual classes in isolation, MFTF drives a real browser through Selenium WebDriver and clicks through storefront and admin area like an actual user. That closes exactly the gap that unit and integration tests leave open: the interplay of PHP backend, layout XML, JavaScript and CSS, which only becomes visible in the rendered browser.

In practice, the value of MFTF shows up especially after module updates or theme changes. A plugin that correctly implements a checkout observer class can still break checkout if a JavaScript module no longer loads or a Knockout binding hits a changed template. Exactly this class of bug is uncovered by MFTF, because it runs through the complete rendering and interaction path in the browser instead of only checking PHP return values.

The boundary matters: MFTF does not replace unit tests or static analysis. It is the third, topmost layer of a test strategy, typically covering few but business critical flows such as adding a product to the cart, completing checkout or saving a configuration form in the admin area. Whoever tries to cover every line of code with MFTF ends up with a test suite that runs for hours instead of minutes and breaks with every small layout change.

2. MFTF setup and directory structure in a custom module

Since Magento 2.3, MFTF has been part of the standard installation via magento/magento2-functional-testing-framework, either as a Composer dependency of magento/mtf-tests or directly as a dev requirement. In a custom module, the entire MFTF test code does not live inside the module folder itself, but in parallel under dev/tests/acceptance/tests/functional/Vendor/Module/Test/. This separation ensures that test code never accidentally ends up in a production deployment.

After the Composer require, the project setup continues through MFTF's own CLI. The command vendor/bin/mftf build:project generates the configuration files codeception.yml and .env, where base URL, backend login and Selenium browser settings are stored. Without a correctly configured .env with a valid MAGENTO_BASE_URL and MAGENTO_BACKEND_NAME, not a single MFTF test class will start.


{
  "require-dev": {
    "magento/magento2-functional-testing-framework": "^4.7",
    "codeception/codeception": "^5.0"
  },
  "extra": {
    "magento-force": "override"
  }
}

Inside the module itself there are subfolders for every test artifact category: Test/ for the actual test scenarios, ActionGroup/ for reusable step sequences, Page/ and Section/ for the DOM structure of pages, and Data/ for test data entities. This clear separation is not an optional style choice but mandatory for MFTF, because the framework interprets files based on their folder and their XML schema.

3. Anatomy of an MFTF test: Test, Data, Page, Section

An MFTF test is always an XML file, never PHP code. That is a deliberate design decision by Adobe: XML test definitions can be merged much more easily across several extensions, because Magento modules can extend or disable another module's tests through XML merging without ever touching its source code. A test consists of a unique name, optional annotations for test case references, and a sequence of steps that are either direct actions or calls to ActionGroups.

Every step in an MFTF test references an element through a Section, never through a hardcoded CSS selector directly inside the test. This indirection is central: if a selector changes in the theme, only the Section file needs to be adjusted, not every single test that uses the element. This exact structure is what keeps MFTF tests maintainable over years, even as the frontend markup changes repeatedly.


<!-- dev/tests/acceptance/tests/functional/Mironsoft/Catalog/Test/AddConfigurableProductToCartTest.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd">
    <test name="AddConfigurableProductToCartTest">
        <annotations>
            <features value="Catalog"/>
            <stories value="Configurable Product"/>
            <title value="Add configurable product with selected options to cart"/>
            <description value="Customer selects size and color, then adds the product to cart"/>
            <severity value="CRITICAL"/>
            <group value="catalog"/>
            <group value="configurable_product"/>
        </annotations>

        <amOnPage url="{{StorefrontProductPage.url(_defaultProduct.urlKey)}}" stepKey="goToProductPage"/>
        <waitForPageLoad stepKey="waitForProductPage"/>

        <actionGroup ref="StorefrontSelectConfigurableOptionActionGroup" stepKey="selectSize">
            <argument name="attributeCode" value="size"/>
            <argument name="optionLabel" value="M"/>
        </actionGroup>

        <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="clickAddToCart"/>
        <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/>
        <see selector="{{StorefrontMessagesSection.success}}" userInput="You added" stepKey="assertSuccessMessage"/>
    </test>
</tests>

Notice the consistent use of stepKey: every step in MFTF needs a unique identifier within the test. This allows extensions to insert a single step at a precise position using before or after attributes, instead of having to rewrite the entire test. This merge capability is one of the biggest practical advantages over classic Codeception PHP code.

4. Modeling Page Objects and Section Objects properly

Page Objects define a URL together with its parameters in MFTF, while Section Objects encapsulate the individual DOM elements of that page as named selectors. This separation follows the classic Page Object pattern from test automation and is mandatory in MFTF: no test may contain a raw CSS or XPath selector directly, it must always reference a named element through {{SectionName.elementName}}.

A common beginner mistake is creating a separate Page and Section file for every tiny page variant. It is much more sensible to bundle Sections by business responsibility, for example a single StorefrontProductActionSection for all purchase actions on the product page, regardless of whether it is a simple or configurable product. That reduces duplication and keeps the MFTF structure manageable even as the test suite grows.


<!-- dev/tests/acceptance/tests/functional/Mironsoft/Catalog/Page/StorefrontProductPage.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageSchema.xsd">
    <page name="StorefrontProductPage" url="/{{urlKey}}.html" module="Mironsoft_Catalog" area="storefront" parameterized="true">
        <section name="StorefrontProductActionSection"/>
    </page>
</pages>

<!-- dev/tests/acceptance/tests/functional/Mironsoft/Catalog/Section/StorefrontProductActionSection.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionSchema.xsd">
    <section name="StorefrontProductActionSection">
        <element name="addToCart" type="button" selector="#product-addtocart-button" timeout="30"/>
        <element name="qty" type="input" selector="#qty" timeout="10"/>
        <element name="swatchOption" type="text" selector=".swatch-option[option-label='{{var1}}']" parameterized="true"/>
    </section>
</sections>

The attribute parameterized="true" allows placeholders such as {{var1}} to be used in selectors, which get filled with concrete values at test call time. This lets a single selector element be reused for every swatch option of a configurable product, instead of maintaining a separate selector for every color and size. For MFTF projects with many product variants, this is the decisive lever against maintenance overhead.

5. ActionGroups: building reusable test steps

ActionGroups are the central reuse mechanism in MFTF. Instead of writing out the login process or the selection of a configuration option again in every test, an ActionGroup encapsulates that step sequence once with defined input parameters. Every test that needs this functionality simply calls the ActionGroup by reference. This reduces not only duplication but also eases maintenance: if the login flow changes, only the ActionGroup needs to be updated, not every individual test.

ActionGroups additionally support inheritance through extends, so a specialized variant of a base ActionGroup only needs to define the steps that differ. This is particularly useful when two very similar flows exist, for example adding a simple product versus a configurable product to the cart, which differ only in a few intermediate steps.


<!-- dev/tests/acceptance/tests/functional/Mironsoft/Catalog/ActionGroup/StorefrontSelectConfigurableOptionActionGroup.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd">
    <actionGroup name="StorefrontSelectConfigurableOptionActionGroup">
        <annotations>
            <description value="Selects a configurable product option by attribute code and label"/>
        </annotations>
        <arguments>
            <argument name="attributeCode" type="string"/>
            <argument name="optionLabel" type="string"/>
        </arguments>

        <waitForElementVisible selector="{{StorefrontProductActionSection.swatchOption(optionLabel)}}" stepKey="waitForSwatch"/>
        <click selector="{{StorefrontProductActionSection.swatchOption(optionLabel)}}" stepKey="clickSwatch"/>
        <waitForElementNotVisible selector="{{StorefrontProductActionSection.optionLoader}}" stepKey="waitForPriceUpdate"/>
    </actionGroup>
</actionGroups>

A second, often underestimated advantage of ActionGroups is the testability of the test infrastructure itself. If a shared login or setup ActionGroup is broken, all dependent tests fail immediately, which makes the root cause visible instead of hiding it inside dozens of individual tests. Good MFTF projects therefore invest disproportionate care in the base ActionGroups for login, navigation and product selection.

6. Generating and running MFTF tests

MFTF tests are never executed directly from XML, they are first compiled into PHP Codeception classes. The command vendor/bin/mftf generate:tests reads all XML test files, performs merges between base and extension modules, and writes runnable PHP code to dev/tests/acceptance/tests/_generated/. This step already surfaces errors in the XML structure or missing references before the actual test run even starts.

The actual execution then happens through Codeception, usually wrapped by the command vendor/bin/mftf run:test . In the Mark Shust Docker setup, this typically happens inside the container via bin/cli vendor/bin/mftf run:test AddConfigurableProductToCartTest, with Selenium provided either as its own container or through a cloud testing platform.


#!/usr/bin/env bash
# Build MFTF project configuration once after composer require
bin/cli vendor/bin/mftf build:project

# Generate PHP/Codeception test classes from XML definitions
bin/cli vendor/bin/mftf generate:tests

# Run a single test by name
bin/cli vendor/bin/mftf run:test AddConfigurableProductToCartTest

# Run all tests tagged with a specific group
bin/cli vendor/bin/mftf run:group catalog

# Run the entire suite headless, useful for CI
bin/cli vendor/bin/mftf run:test --remove-generated-and-run-only

For local development, the flag -k or --keep-generated is worth using, since it prevents the generated PHP files from being deleted after every run. That way, the compiled Codeception code can be inspected directly whenever an MFTF test behaves differently than expected and the XML layer alone does not explain why.

7. Debugging failed MFTF tests

A failing MFTF test produces a screenshot of the browser state at the moment of failure by default, along with an HTML snapshot of the page, both stored under dev/tests/acceptance/tests/_output/. That is the first place to look at every failure: a glance at the screenshot usually shows immediately whether a cookie banner blocked the click, a loading indicator was still visible, or the expected element simply does not exist.

Timing issues are by far the most common cause of failure in MFTF tests. A click on an element that is not yet interactive due to an asynchronous price calculation leads to a flaky test that sometimes passes and sometimes fails. The fix is consistent use of waitForElementVisible, waitForElementNotVisible and waitForPageLoad instead of fixed wait second values, which are either too short or unnecessarily long.

For deeper debugging, MFTF supports the MFTF_DEBUG=1 mode in the .env file, which logs every step individually and slows down execution. Combined with a visible Chrome instance instead of headless mode, this makes it possible to observe live exactly where the expected interaction diverges from the actual page behavior.

8. Running MFTF in a CI pipeline

Integrating MFTF tests into a CI pipeline carries a different set of demands than running individual tests locally. A complete MFTF run needs a working Magento instance with test data, a Selenium container or a remote WebDriver connection, and enough timeout headroom, since browser interactions are naturally slower than PHP unit tests. Without deliberate isolation of the test database, parallel pipeline runs quickly collide with each other.

A proven approach is to run MFTF not on every commit, but specifically before merges into the main branch, or as a full nightly regression run. Critical, business relevant flows such as checkout and product configuration run on every merge request, while the complete test catalog runs nightly. That keeps developer feedback time short while still regularly covering the full breadth of scenarios.

An often overlooked point is the stability of the test environment itself. Changing product data, expiring discount campaigns or shifting prices in the CI database lead to tests that suddenly fail without any code change. Every MFTF pipeline should therefore start from a reproducible database fixture that gets freshly loaded before every run, instead of relying on a constantly changing staging data set.

9. MFTF compared to unit and integration tests

The decision about which test level should cover a particular behavior is not a matter of taste, it has direct consequences for runtime, maintenance effort and expressiveness. MFTF is the right choice for flows that involve multiple system layers and the browser, but the wrong choice for verifying individual business rules.

Test level Tool Checks Runtime per test
Unit tests PHPUnit Isolated class logic, business rules Milliseconds
Integration tests PHPUnit + Magento test framework Interplay of DI, DB and modules Seconds
MFTF MFTF + Codeception + Selenium Complete click path in a real browser Ten seconds to minutes
Manual QA Human Exploratory testing, visual detail Minutes to hours

The practical rule of thumb: business logic belongs in unit tests, the interplay of several Magento components belongs in integration tests, and only the truly critical end to end paths, where the browser itself is part of the risk, belong in MFTF. A test pyramid with many unit tests, fewer integration tests and few but targeted MFTF tests stays fast, stable and expressive at the same time.

Mironsoft

MFTF test automation, CI integration and Magento 2 quality assurance

Want checkout and configurator covered automatically?

We build MFTF test suites for your business critical Magento 2 flows, from directory structure through ActionGroups to a stable CI pipeline with reproducible test data.

MFTF setup

Setting up project structure, Page Objects and base ActionGroups cleanly

Test coverage

Automating checkout, configurator and admin forms where it matters

CI integration

Integrating stable, reproducible MFTF runs into your pipeline

10. Summary

MFTF closes a gap in Magento 2 that no other test level can cover: the behavior of the complete system in a real browser, including JavaScript, CSS and rendering. The clear separation into Test, Page, Section, ActionGroup and Data keeps test suites maintainable even as frontend markup changes repeatedly over years. Whoever consistently encapsulates selectors in Section files and extracts recurring flows into ActionGroups builds an MFTF suite that grows with the project instead of being rewritten with every release.

The decisive success factor is discipline in choosing which scenarios to cover. MFTF is suited to a small number of business critical end to end paths, not to full coverage of every line of code. Combined with a stable CI pipeline, reproducible test data and consistent wait strategies, MFTF becomes a reliable safety net that catches real regressions before they become visible in the live shop.

MFTF in Magento 2: the essentials at a glance

What MFTF is

Adobe's framework for browser driven end to end tests, positioned above unit and integration tests.

Structure

Test, ActionGroup, Page, Section and Data as separate XML files with clear responsibilities.

Stability

Explicit waits instead of fixed second values are the most important fix for flaky MFTF tests.

CI operation

Reproducible test data and targeted test selection instead of a full run on every commit.

11. FAQ: MFTF in Magento 2

1What is MFTF in Magento 2?
Adobe's framework for browser driven end to end tests via Selenium WebDriver that replays complete user flows in storefront and admin area.
2How does MFTF differ from PHPUnit?
PHPUnit checks isolated logic in PHP, MFTF checks the complete flow in a real browser including rendering and JavaScript.
3Where do MFTF tests live inside a module?
Under dev/tests/acceptance/tests/functional/Vendor/Module, separated from the actual production code.
4Why no direct CSS selectors in the test?
Selectors live in Section files. If the theme changes, only the Section needs to be adjusted, not every test.
5What is an ActionGroup?
A reusable step sequence with arguments, such as login or product selection, used by reference from multiple tests.
6How do I run a single test?
With vendor/bin/mftf run:test TestName, after a prior generate:tests, in a Docker setup via bin/cli.
7Why is my test flaky?
Usually missing explicit waits. waitForElementVisible instead of fixed second values resolves most cases.
8Should MFTF run on every commit?
Usually not in full. Critical flows on every merge request, the full catalog tends to run nightly.
9Can MFTF replace unit tests?
No. MFTF covers a small number of critical end to end paths, business rules remain the job of fast unit tests.
10How do you debug a failed test?
Via the screenshot and HTML snapshot in the _output directory, combined with MFTF_DEBUG=1 and a visible instead of headless browser.