Tips and Tricks

Sometimes, little changes can make a big difference in your project. Here are some test writing tips to keep everything running smoothly.

Actions and action groups

Use parameterized selectors in action groups with argument references

Clarity and readability are important factors in good test writing. Having to parse through unreadable code can be time consuming. Save time by writing clearly. The good example clearly shows what the selector arguments refer to. In the bad example we see two parameters being passed into the selector with little clue as to their purpose.

Why? The next person maintaining the test or extending it may not be able to understand what the parameters are referencing.

Good

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<test>
    <actionGroup ref="VerifyOptionInProductStorefront" stepKey="verifyConfigurableOption" after="AssertProductInStorefrontProductPage">
        <argument name="attributeCode" value="$createConfigProductAttribute.default_frontend_label$"/>
        <argument name="optionName" value="$createConfigProductAttributeOption1.option[store_labels][1][label]$"/>
    </actionGroup>
</test>

<actionGroup name="VerifyOptionInProductStorefront">
    <arguments>
        <argument name="attributeCode" type="string"/>
        <argument name="optionName" type="string"/>
    </arguments>
    <seeElement selector="{{StorefrontProductInfoMainSection.attributeOptionByAttributeID(attributeCode, optionName)}}" stepKey="verifyOptionExists"/>
</actionGroup>

Bad

1
2
3
<test>
    <seeElement selector="{{StorefrontProductInfoMainSection.attributeOptionByAttributeID($createConfigProductAttribute.default_frontend_label$, $createConfigProductAttributeOption1.option[store_labels][1][label]$)}}" stepKey="verifyOptionExists"/>
</test>

Perform the most critical actions first in the <after> block

Perform non-browser driving actions first. These are more likely to succeed as no UI is involved. In the good example, magentoCLI and deleteData are run first to ensure a proper state. In the bad example, we perform some heavy UI steps first.

Why? If something goes wrong there, then the critical magentoCLI commands may not get a chance to run, leaving Magento configured incorrectly for any upcoming tests.

Good:

1
2
3
4
5
6
7
8
9
10
11
12
13
<after>
    <magentoCLI command="indexer:set-mode" arguments="schedule" stepKey="setIndexerMode"/>
    <magentoCLI command="config:set catalog/frontend/flat_catalog_category 0" stepKey="setFlatCatalogCategory"/>
    <deleteData createDataKey="category" stepKey="deleteCategory"/>
    <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/>
    <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn">
        <argument name="customStore" value="customStoreEN"/>
    </actionGroup>
    <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr">
        <argument name="customStore" value="customStoreFR"/>
    </actionGroup>
    <actionGroup ref="logout" stepKey="logout"/>
</after>

Bad:

1
2
3
4
5
6
7
8
9
10
11
12
13
<after>
    <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn">
        <argument name="customStore" value="customStoreEN"/>
    </actionGroup>
    <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr">
        <argument name="customStore" value="customStoreFR"/>
    </actionGroup>
    <deleteData createDataKey="category" stepKey="deleteCategory"/>
    <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/>
    <magentoCLI command="config:set catalog/frontend/flat_catalog_category 0" stepKey="setFlatCatalogCategory"/>
    <magentoCLI command="indexer:set-mode" arguments="schedule" stepKey="setIndexerMode"/>
    <actionGroup ref="logout" stepKey="logout"/>
</after>

When to use see vs. seeElement

Use see and seeElement wisely. If you need to see some element and verify that the text inside is shown correctly, use the see action. If you need to verify that element present on page, use seeElement. But never use seeElement and build a xPath which contains the expected text.

Why? For see it will output something similar to this: Failed asserting that any element by #some_selector contains text "some_text" And for seeElement it will output something like this: Element by #some_selector is not visible. There is a subtle distinction: The first is a failure but it is the desired result: a ‘positive failure’. The second is a proper result of the action.

Good:

1
<see selector="//div[@data-element='content']//p" userInput="SOME EXPECTED TEXT" stepKey="seeSlide1ContentStorefront"/>

Bad:

1
<seeElement selector="//div[@data-element='content']//p[.='SOME EXPECTED TEXT']" stepKey="seeSlide1ContentStorefront"/>

Always specify a default value for action group arguments

Whenever possible, specify a defaultValue for action group arguments.

GOOD:

1
2
3
4
5
6
7
8
9
10
11
12
13
<actionGroup name="StorefrontAssertProductImagesOnProductPageActionGroup">
    <arguments>
        <argument name="productImage" type="string" defaultValue="Magento_Catalog/images/product/placeholder/image.jpg" />
    </arguments>
    <waitForElementNotVisible selector="{{StorefrontProductMediaSection.gallerySpinner}}" stepKey="waitGallerySpinnerDisappear" />
    <seeElement selector="{{StorefrontProductMediaSection.gallery}}" stepKey="seeProductGallery" />
    <seeElement selector="{{StorefrontProductMediaSection.productImage(productImage)}}" stepKey="seeProductImage" />
    <click selector="{{StorefrontProductMediaSection.productImage(productImage)}}" stepKey="openFullscreenImage" />
    <waitForPageLoad stepKey="waitForGalleryLoaded" />
    <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(productImage)}}" stepKey="seeFullscreenProductImage" />
    <click selector="{{StorefrontProductMediaSection.closeFullscreenImage}}" stepKey="closeFullScreenImage" />
    <waitForPageLoad stepKey="waitForGalleryDisappear" />
</actionGroup>

BAD:

1
2
3
4
5
6
7
8
9
10
11
12
13
<actionGroup name="StorefrontAssertProductImagesOnProductPageActionGroup">
    <arguments>
        <argument name="productImage" type="string" />
    </arguments>
    <waitForElementNotVisible selector="{{StorefrontProductMediaSection.gallerySpinner}}" stepKey="waitGallerySpinnerDisappear" />
    <seeElement selector="{{StorefrontProductMediaSection.gallery}}" stepKey="seeProductGallery" />
    <seeElement selector="{{StorefrontProductMediaSection.productImage(productImage)}}" stepKey="seeProductImage" />
    <click selector="{{StorefrontProductMediaSection.productImage(productImage)}}" stepKey="openFullscreenImage" />
    <waitForPageLoad stepKey="waitForGalleryLoaded" />
    <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(productImage)}}" stepKey="seeFullscreenProductImage" />
    <click selector="{{StorefrontProductMediaSection.closeFullscreenImage}}" stepKey="closeFullScreenImage" />
    <waitForPageLoad stepKey="waitForGalleryDisappear" />
</actionGroup>

Build tests from action groups

Build your tests using action groups, even if an action group contains a single action.

Why? For extension developers, this will make it easier to extend or customize tests. Extending a single action group will update all tests that use this group. This improves maintainability as multiple instances of a failure can be fixed with a single action group update.

GOOD:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<test name="NavigateClamberWatchEntityTest">
    <annotations>
        <!--some annotations-->
    </annotations>

    <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage">
        <argument name="productUrl" value="{{ClamberWatch.url_key}}" />
    </actionGroup>
    <actionGroup ref="StorefrontAssertProductNameOnProductPageActionGroup" stepKey="assertProductName">
        <argument name="productName" value="{{ClamberWatch.name}}" />
    </actionGroup>
    <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="assertProductSku">
        <argument name="productSku" value="{{ClamberWatch.sku}}" />
    </actionGroup>
    <actionGroup ref="StorefrontAssertProductPriceOnProductPageActionGroup" stepKey="assertProductPrice">
        <argument name="productPrice" value="{{ClamberWatch.price}}" />
    </actionGroup>
    <actionGroup ref="StorefrontAssertProductImagesOnProductPageActionGroup" stepKey="assertProductImage">
        <argument name="productImage" value="{{ClamberWatch.image}}" />
    </actionGroup>
</test>

BAD:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<test name="NavigateClamberWatchEntityTest">
    <annotations>
                <!--some annotations-->
    </annotations>

    <amOnPage url="{{StorefrontProductPage.url(ClamberWatch.url_key)}}" stepKey="openProductPage"/>
    <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{ClamberWatch.name}}" stepKey="seeProductName" />
    <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{ClamberWatch.sku}}" stepKey="seeProductSku" />
    <see selector="{{StorefrontProductInfoMainSection.price}}" userInput="{{ClamberWatch.price}}" stepKey="seeProductPrice" />
    <waitForElementNotVisible selector="{{StorefrontProductMediaSection.gallerySpinner}}" stepKey="waitGallerySpinnerDisappear" />
    <seeElement selector="{{StorefrontProductMediaSection.gallery}}" stepKey="seeProductGallery" />
    <seeElement selector="{{StorefrontProductMediaSection.productImage(ClamberWatch.productImage)}}" stepKey="seeProductImage" />
    <click selector="{{StorefrontProductMediaSection.productImage(ClamberWatch.productImage)}}" stepKey="openFullscreenImage" />
    <waitForPageLoad stepKey="waitForGalleryLoaded" />
    <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(ClamberWatch.productImage)}}" stepKey="seeFullscreenProductImage" />
</test>

Use descriptive stepKey names

Make stepKeys values as descriptive as possible. Do not use numbers to make a stepKey unique.

Why? This helps with readability and clarity.

GOOD:

1
2
3
4
5
6
7
8
9
10
11
<click selector="{{StorefrontNavigationSection.topCategory(SimpleSubCategory.name)}}" stepKey="clickSimpleSubCategoryLink" />
<waitForPageLoad stepKey="waitForSimpleSubCategoryPageLoad" />
<click selector="{{StorefrontCategoryMainSection.productLinkByHref(SimpleProduct.urlKey)}}" stepKey="clickSimpleProductLink" />
<waitForPageLoad stepKey="waitForSimpleProductPageLoad" />

<!-- Perform some actions / Assert product page -->

<click selector="{{StorefrontNavigationSection.topCategory(CustomCategory.name)}}" stepKey="clickCustomCategoryLink" />
<waitForPageLoad stepKey="waitForCustomCategoryPageLoad" />
<click selector="{{StorefrontCategoryMainSection.productLinkByHref(CustomSimpleProduct.urlKey)}}" stepKey="clickCustomSimpleProductLink" />
<waitForPageLoad stepKey="waitForCustomSimpleProductPageLoad" />

BAD:

1
2
3
4
5
6
7
8
9
10
11
<click selector="{{StorefrontNavigationSection.topCategory(SimpleSubCategory.name)}}" stepKey="clickCategoryLink1" />
<waitForPageLoad stepKey="waitForPageLoad1" />
<click selector="{{StorefrontCategoryMainSection.productLinkByHref(SimpleProduct.urlKey)}}" stepKey="clickProductLink1" />
<waitForPageLoad stepKey="waitForPageLoad2" />

<!-- Perform some actions / Assert product page -->

<click selector="{{StorefrontNavigationSection.topCategory(CustomCategory.name)}}" stepKey="clickCategoryLink2" />
<waitForPageLoad stepKey="waitForPageLoad3" />
<click selector="{{StorefrontCategoryMainSection.productLinkByHref(CustomSimpleProduct.urlKey)}}" stepKey="clickProductLink2" />
<waitForPageLoad stepKey="waitForPageLoad4" />

Exception:

Use numbers within stepKeys when order is important, such as with testing sort order.

1
2
3
4
<createData entity="BasicMsiStock1" stepKey="createCustomStock1"/>
<createData entity="BasicMsiStock2" stepKey="createCustomStock2"/>
<createData entity="BasicMsiStock3" stepKey="createCustomStock3"/>
<createData entity="BasicMsiStock4" stepKey="createCustomStock4"/>

Selectors

Use contains() around text()

When possible, use contains(text(), 'someTextHere') rather than text()='someTextHere'. contains() ignores whitespace while text() accounts for it.

Why? If you are comparing text within a selector and have an unexpected space, or a blank line above or below the string, text() will fail while the contains(text()) format will catch it. In this scenario text() is more exacting. Use it when you need to be very precise about what is getting compared.

GOOD:

//span[contains(text(), 'SomeTextHere')]

BAD:

//span[text()='SomeTextHere']

Build selectors in proper order

When building selectors for form elements, start with the parent context of the form element. Then specify the element name attribute in your selector to ensure the correct element is targeted. To build a selector for an input, use the pattern: {{section_selector}} {{input_selector}} or for a button: {{section_selector}} {{button_selector}}

Why? Traversing the DOM takes a finite amount of time and reducing the scope of the selector makes the selector lookup as efficient as possible.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
<div class="admin__field _required" data-bind="css: $data.additionalClasses, attr: {'data-index': index}, visible: visible" data-index="name">
    <div class="admin__field-label" data-bind="visible: $data.labelVisible">
        <span data-bind="attr: {'data-config-scope': $data.scopeLabel}, i18n: label" data-config-scope="[STORE VIEW]">Product Name</span>
    </div>
    <div class="admin__field-control" data-bind="css: {'_with-tooltip': $data.tooltip, '_with-reset': $data.showFallbackReset && $data.isDifferedFromDefault}">
        <input class="admin__control-text" type="text" name="product[name]" aria-describedby="notice-EXNI71H" id="EXNI71H" maxlength="255" data-bind="
            attr: {
                name: inputName,
                placeholder: placeholder,
                maxlength: 255}"/>
    </div>
</div>

GOOD:

1
<element name="productName" type="input" selector="*[data-index='product-details'] input[name='product[name]']"/>

BAD:

1
<element name="productName" type="input" selector=".admin__field[data-index=name] input"/>

Build selectors with appropriate specificity

Selectors that are too general might sweep up unexpected elements. When possible, select the first parent tag and then specify the desired element within that selection.

Why? Elements that are overly specific are less flexible and may fail if unexpected DOM changes occur. It also reduces the amount of the DOM it needs to parse.

GOOD:

1
2
3
 form[name='myform'] > input[name='firstname']

 //*[@id='container'][@class='dashboard-title']

BAD:

1
2
3
 input[name='firstname']

 //*[@id='container']/*[@class='dashboard-advanced-reports']/*[@class='dashboard-advanced- reports-description']/*[@class='dashboard-title']

General tips

Use data references to avoid hardcoded values

If you need to run a command such as <magentoCLI command="config:set" />, do not hardcode paths and values to the command. Rather, create an appropriate ConfigData.xml file, which contains the required parameters for running the command. It will simplify the future maintanence of tests.

GOOD:

1
<magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" />

BAD:

1
<magentoCLI command="config:set customer/captcha/length 3" stepKey="setCaptchaLength" />

For example: This test refers to this Data file.

Use descriptive variable names

Use descriptive variable names to increase readability. Why? It makes the code easier to follow and update.

GOOD:

1
<element name="storeName" type="checkbox" selector="//label[contains(text(),'{{storeName}}')]" parameterized="true"/>

BAD:

1
<element name="storeName" type="checkbox" selector="//label[contains(text(),'{{var1}}')]" parameterized="true"/>

Use proper checkbox actions

When working with input type checkbox, do not use the click action; use checkOption or uncheckOption instead. Why? A click does not make it clear what the ending state will be; it will simply toggle the current state. Using the proper actions will ensure the expected state of the checkbox.

GOOD:

1
2
<checkOption selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="selectSecondWebsite"/>
<uncheckOption selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="unselectSecondWebsite"/>

BAD:

1
2
<click selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="selectSecondWebsite"/>
<click selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="unselectSecondWebsite"/>