Online store tutorial
In this tutorial, we will be exploring how to use Provengo to conduct tests on the Magento online store.
We will begin by defining the necessary actions that a typical user might take while shopping in our online store. This includes actions such as browsing products, adding items to their shopping carts, removing some items, and finally, proceeding to checkout. We will then use these actions to define tests that mimic the behavior of multiple users interacting with our store simultaneously.
- When completing this tutorial, you will acquire proficiency in the following
-
-
How to define actions for the store and implement these actions using Selenium.
-
How to define user stories and run them in parallel sessions.
-
How to instantiate a pre-test script.
-
How to create and run tests on a real-life web application.
-
- This is useful because
-
This is a real-world example, showcasing some classic flow, screens, and features that are very common in web applications.
- Code Companion
- Pre-Requisites
Magento Store Overview
Magento is a versatile open-source e-commerce platform, crafted in PHP. Its inception was by Varien Inc., a US-based private company, with its first public release in 2008. Adobe later acquired Magento in May 2018. Magento’s primary function is to empower businesses to construct and manage online stores effortlessly. Its robust features, scalability, and customizable templates enable businesses of all sizes to elevate their online sales.
In this tutorial, we will explore key features of Magento, an e-commerce platform, and demonstrate how to test these functionalities using Provengo. The features include:
-
Login: This is the initial step where users enter their credentials to access the store.
-
Add to Cart: As users browse products, they can add desired items to their virtual shopping cart.
-
Remove from Cart: Users can remove items from their cart if they change their mind.
-
Checkout: This is the final step where users review their order, provide shipping details, choose a payment method, and confirm their purchase.
By using Magento as an example, we aim to illustrate how Provengo can be effectively utilized to test specific functionalities of a web application, ensuring a smooth and efficient user experience
The defineAction
function
We begin by introducing a utility function, defineAction
. This function simplifies the creation of custom actions and their conversion into Selenium steps. Upon invocation, these actions initiate a series of events and execute a designated function, all while ensuring no disruption from other actions within the same session.
l run concurrently.
Create a new file named actions.js
in the spec/js
folder. Input the following code into this file:
defineAction = function (name, func) {
if (!/^[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*$/.test(name)) {
throw new Error('Name must be a valid identifier');
}
if (typeof func !== 'function') {
throw new Error('Func must be a function');
}
const AnyStartInSession = function (s) {
return EventSet("AnyStartInSession-" + s, function (e) {
return e.data !== null && e.data.hasOwnProperty('startEvent') && e.data.startEvent && String(s).equals(e.data.session.name);
});
};
SeleniumSession.prototype[name] = function (data) {
data = Object.assign({ session: this, startEvent: true }, data);
sync({ request: Event(`Start(${name})`, data) });
block(AnyStartInSession(this.name), function () {
func(data.session, data);
});
sync({ request: Event(`End(${name})`, data) });
};
};
The defineAction
function is a higher-order function that takes two arguments: name
and func
.
-
name
is a string that represents the name of the action. It must be a valid identifier, which is checked using a regular expression. -
func
is a function that will be executed when the action is invoked. An error is thrown iffunc
is not a function.
The function also defines a helper function AnyStartInSession
, which creates an event set for any start event in a given session s
.
The defineAction
function then adds a new method to the SeleniumSession
prototype with the name name
. This method:
-
Takes an argument
data
, which is an object. -
Adds the session and a start event flag to
data
. -
Sends a start event.
-
Blocks any other start event in the same session and executes
func
. -
Sends an end event.
This allows to define custom actions for a Selenium session that can be invoked with session-specific data. The actions are executed atomically within the session, meaning no other start events in the same session can interfere.
Defining the actions
Login
Now, let’s define a custom action for logging into an account. Type the following code into actions.js
:
defineAction("Login", function (session, event) {
with (session) {
click("//button[contains(@class,'accountTrigger-trigger-23q clickable-root-1HB')]");
waitForVisibility("//span[contains(@class,'signIn-title-2hm capitalize')]", 1000);
writeText("css::input#email", event.username);
writeText("css::input#Password", event.password);
click("//span[text()='Sign-in to Your Account']/following::span[text()='Sign In']");
if (event.expectedWelcome)
waitForVisibility("(//span[text()='" + event.expectedWelcome + "'])[2]", 10000)
}
});
This Login
action performs the following steps:
-
Clicks on the button that triggers the login form.
-
Waits for the sign-in title to become visible.
-
Writes the username and password into the respective input fields.
-
Clicks on the 'Sign In' button.
-
If an expected welcome message is provided, it waits for this message to become visible.
This action encapsulates the entire login process into a single, reusable function. It takes an event
object as an argument, which should contain the username
, password
, and optionally an expectedWelcome
message. This makes it easy to use this action in different scenarios with different accounts.
AddToCart
Next, let’s define a custom action for adding a product to the cart. Type the following code into actions.js
:
defineAction("AddToCart", function (session, event) {
with (session) {
click("//div[@id='root']/main[1]/header[1]/div[1]/div[1]/button[1]");
click("//span[text()='" + event.product.category + "']");
click("//span[text()='" + event.product.subCategory + "']");
click("//span[text() = '" + event.product.product + "']");
for (let opt in event.product.options) {
click("//button[@title='" + event.product.options[opt] + "']");
waitForVisibility("//button[@title='" + event.product.options[opt] + "' and contains(@class,'selected')]", 50000);
}
if (event.product.quantity) {
writeText("//input[@name='quantity']", event.product.quantity, true);
}
click("//span[text()='Add to Cart']");
click("//div[@id='root']/main[1]/header[1]/div[1]/div[1]/button[1]");
click("//span[text()='Main Menu']/preceding::button");
click("//span[text()='Main Menu']/preceding::button");
}
});
The AddToCart
action performs the following steps:
-
Opens the menu.
-
Navigates to the specified product category and sub-category.
-
Selects the specified product.
-
Selects the specified product options and verifies their selection.
-
If a quantity is specified, it inputs the quantity.
-
Adds the product to the cart.
-
Returns to the main menu.
This action encapsulates the entire process of adding a product to the cart into a single, reusable function. It takes an event
object as an argument, which should contain the product
object with category
, subCategory
, product
, options
, and optionally quantity
. This makes it easy to use this action in different scenarios with different products.
RemoveFromCart
Next, let’s define a custom action for removing a product from the cart. Type the following code into actions.js
:
defineAction("RemoveFromCart", function (session, event) {
with (session) {
runCode(`document.querySelectorAll('button[class*="cartTrigger"]')[0].click()`);
click(`//div[contains(@class,'productList')]//a[contains(.,'${event.product.product}')]/following-sibling::button[contains(@class,'deleteButton')]`);
runCode(`document.querySelectorAll('button[class*="cartTrigger"]')[0].click()`);
}
});
The RemoveFromCart
action performs the following steps:
-
Opens the cart.
-
Clicks the remove button for the specified product.
-
Closes the cart.
This action encapsulates the entire process of removing a product from the cart into a single, reusable function. It takes an event
object as an argument, which should contain the product
object with the product
name. This makes it easy to use this action in different scenarios with different products.
Checkout
Next, let’s define a custom action for checking out a cart. Type the following code into actions.js
:
defineAction("CheckOut", function (session, event) {
with (session) {
runCode(`document.querySelectorAll('button[class*="cartTrigger"]')[0].click()`);
click("//span[text()='CHECKOUT']");
waitForVisibility("//*[text()='Credit Card']", 20000);
click('//*[@id="paymentMethod--braintree"]');
switchFrame("//iframe[@id='braintree-hosted-field-cardholderName']");
writeText("//input[@id='cardholder-name']", event.user.cardHolderName);
switchFrame("Main Frame");
switchFrame("//iframe[contains(@id,'braintree-hosted-field-number')]");
writeText("//input", event.user.cardNumber);
switchFrame("Main Frame");
switchFrame("//iframe[@id='braintree-hosted-field-expirationDate']");
writeText("//input[@id='expiration']", event.user.expirationDate);
switchFrame("Main Frame");
switchFrame("//iframe[@id='braintree-hosted-field-cvv']");
writeText("//input[@id='cvv']", event.user.cvv);
switchFrame("Main Frame");
click("//span[text()='Review Order']");
if (event.verifyItems) {
for (item of event.verifyItems) {
waitForVisibility("//img[@alt='" + item.product + "']", 5000);
}
}
if (event.verifyNonexistenceOfItems) {
for (item of event.verifyNonexistenceOfItems) {
checkNonExistance("//img[@alt='" + item.product + "']", 5000);
}
}
click("//span[text()='Place Order']");
waitForVisibility("//*[contains(., 'Thank you for your order!')]", 1000000);
}
});
The CheckOut
action performs the following steps:
-
Opens the cart.
-
Clicks the checkout button.
-
Waits for the checkout page to load.
-
Selects the credit card payment method.
-
Inputs the card holder name, card number, expiration date, and CVV into the respective fields.
-
Clicks the 'Review Order' button.
-
If specified, verifies that certain items are in the cart.
-
If specified, verifies that certain items are not in the cart.
-
Places the order and waits for the confirmation message.
This action encapsulates the entire checkout process into a single, reusable function. It takes an event
object as an argument, which should contain the user
object with cardHolderName
, cardNumber
, expirationDate
, cvv
, and optionally verifyItems
and verifyNonexistenceOfItems
. This makes it easy to use this action in different scenarios with different users and products.
Defining the data
We have reached the point where we are prepared to compose the actual test script. With the necessary groundwork laid out, we can now proceed to articulate the series of commands and verifications that will constitute our comprehensive test scenario.
Add the following code in a file called spec/js/_data.js
:
var users = [
{ username: 'roni_cost@example.com', password: 'roni_cost3@example.com', expectedWelcome: 'Hi, Veronica', cardHolderName: "Roni Cost", cardNumber: "3566000020000410", expirationDate: "02/26", cvv: "123" },
{ username: 'david_lowcost@example.com', password: 'david_lowcost3@example.com', expectedWelcome: 'Hi, David', cardHolderName: "David Lowcost", cardNumber: "3566000020000410", expirationDate: "02/26", cvv: "123" },]
var products = [
{ category: 'Tops', subCategory: "All Tops", product: 'Carina Cardigan', options: ['Peach', 'L'], quantity: "1", price: "$54.00" },
{ category: 'Bottoms', subCategory: "Skirts", product: 'Rowena Skirt', options: ['Khaki', 'S'], quantity: "1", price: "$78.00" },
]
This JavaScript code defines two arrays of objects: users
and products
.
The users
array contains two objects, each representing a user. Each user object has properties for username
, password
, expectedWelcome
, cardHolderName
, cardNumber
, expirationDate
, and cvv
.
The products
array contains two objects, each representing a product. Each product object has properties for category
, subCategory
, product
, options
, quantity
, and price
.
These arrays will soon serve as parameters for our tests. Keeping the data separate from the model itself simplifies maintenance. Parameterized models not only enhance clarity but also make it easier to comprehend and generalize.
Defining the automation script
Add the following code snippet to a file called spec/js/tests.js
:
/* @provengo summon selenium */
const URL = "https://master-7rqtwti-c5v7sxvquxwl4.eu-4.magentosite.cloud/";
const NUM_OF_USERS = 2;
const NUM_OF_PRODUCTS_PER_USER = 2;
// Run sessions for each user in the users array (up to NUM_OF_USERS)
users.slice(0, NUM_OF_USERS).forEach(user => {
bthread('Add to cart session for ' + user.username, function () {
with (new SeleniumSession().start(URL)) {
let addedProducts = new Set();
// Login
Login(user);
// Add products to cart
while (addedProducts.size < NUM_OF_PRODUCTS_PER_USER) {
let product = choose(products.filter(product => !addedProducts.has(product)));
addedProducts.add(product);
AddToCart({ product: product, user: user });
}
// Remove a product from cart
let product = choose(Array.from(addedProducts));
addedProducts.delete(product);
RemoveFromCart({ product: product, user: user });
// Checkout
if (addedProducts.size !== 0) {
let notAdded = products.filter(product => !addedProducts.has(product));
CheckOut({ shippingMethod: 'Fixed', user: user, verifyItems: Array.from(addedProducts), verifyNonexistenceOfItems: notAdded });
}
}
});
});
This script is designed for automated testing using the Selenium framework. It interacts with a web application by simulating user actions such as logging in, adding products to the cart, removing a product, and completing the checkout process. The script demonstrates the integration of previously defined custom actions to facilitate modularity and reusability in the testing process.
Pre-test script
Our testing process operates under the assumption that the shopping cart is devoid of any items at the commencement of the test. This assumption is crucial as it establishes a consistent starting point for each test run, ensuring that the results are reliable and repeatable.
Therefore, to align with this assumption and to maintain the integrity of our testing environment, it is necessary to clear the cart prior to initiating the test. This step involves removing any items that may have been inadvertently left in the cart from previous operations.
By doing so, we can ensure that each test begins under the same conditions, thereby eliminating any discrepancies that could arise from a pre-populated cart. This practice ultimately enhances the accuracy of our test results and provides a more precise understanding of the system’s behavior.
To do so, type the following into a file caleed empty_cart.py
:
import json
from requests import post, get, delete, put
URL= "https://master-7rqtwti-c5v7sxvquxwl4.eu-4.magentosite.cloud/"
CREDENTIALS = [
{'username': "roni_cost@example.com", 'password': "roni_cost3@example.com"},
{'username': "david_lowcost@example.com", 'password': "david_lowcost3@example.com"},
]
for credentials in CREDENTIALS:
print(f'Emptying cart for {credentials["username"]}...')
r = post(f'{URL}/rest/default/V1/integration/customer/token', params=credentials)
if r.status_code != 200:
print(f'Error: {r.text}')
continue
token = r.text[1:-1]
header = {'Authorization': f'Bearer {token}'}
r = get(f'{URL}/rest/default/V1/carts/mine', headers=header)
cart = json.loads(r.text)
if "items" in cart:
for item in cart["items"]:
delete(f'{URL}/rest/default/V1/carts/mine/items/{item["item_id"]}', headers=header)
This Python script is used to empty the shopping carts of users in a Magento e-commerce platform. Here’s a step-by-step explanation:
-
The script imports the necessary modules: json for parsing JSON responses, and post, get, delete, put from requests for making HTTP requests.
-
The URL variable is set to the base URL of the Magento site.
-
The CREDENTIALS list contains dictionaries with the username and password for two users.
-
The script then loops over each set of credentials in the CREDENTIALS list.
-
For each user, it sends a POST request to the /rest/default/V1/integration/customer/token endpoint to authenticate the user and get a token. The user’s credentials are sent as parameters in the request.
-
If the response status code is not 200, it prints an error message and skips to the next set of credentials.
-
If the response status code is 200, it extracts the token from the response text, removes the first and last characters (which are quotation marks), and stores it in the token variable.
-
It then creates a headers dictionary with an Authorization field set to
Bearer {token}
. -
It sends a GET request to the
/rest/default/V1/carts/mine`
endpoint to get the current user’s shopping cart. The headers dictionary is sent with the request. -
It parses the response text as JSON and stores it in the cart variable.
-
If the cart dictionary contains an items field, it loops over each item in the items list. For each item, it sends a DELETE request to the
/rest/default/V1/carts/mine/items/{item["item_id"]}`
endpoint to remove the item from the cart. The headers dictionary is sent with the request.
-
Running the system
To run the system type:
provengo run --show --before="python <path>/empty_cart.py" <path>>
If all instructions have been followed correctly, and Provengo is installed, you should observe two concurrent web sessions in operation. These sessions simulate the actions of two users, including adding items, deleting them, and checking out. Throughout this process, the tool systematically verifies diverse conditions to validate the system’s functionality.
if
/switch
/?:
/with
). Knowledge of "advanced" JavaScript frameworks such as Node.js or React is not required.