In the previous articles from the series Design Patterns in Automated Testing, I explained how to make your test automation framework better through the implementation of page objects and facades. When we have to write tests for more complex use case scenarios, it usually gets harder and harder to follow the Open Close Principle, one of SOLID’s primary principles. In this part of the series, I will use the Strategy Design Pattern to create extendable assertions for an e-commerce module.
Definition
In computer programming, the strategy pattern (also known as the policy pattern) is a software design pattern that enables an algorithm‘s behavior to be selected at runtime.
Benefits:
-
Defines a family of algorithms.
-
Encapsulates each algorithm.
-
Makes the algorithms interchangeable within that family.
-
The code is easier to maintain as modifying or understanding strategy does not require you to understand the whole main object.
UML Class Diagram
classDiagram
PurchaseContext --> OrderPurchaseStrategy
OrderPurchaseStrategy <|-- VatTaxOrderPurchaseStrategy
OrderPurchaseStrategy <|-- NoTaxOrderPurchaseStrategy
OrderPurchaseStrategy <|-- CouponCodeOrderPurchaseStrategy
class PurchaseContext {
+PurchaseContext(OrderPurchaseStrategy orderPurchaseStrategy)
+purchaseItem()
}
class OrderPurchaseStrategy {
+assertOrderSummary()
}
class VatTaxOrderPurchaseStrategy {
+assertOrderSummary()
}
class NoTaxOrderPurchaseStrategy {
+assertOrderSummary()
}
class CouponCodeOrderPurchaseStrategy {
+assertOrderSummary()
}
Participants
The classes and objects participating in this pattern are:
-
Strategy (OrderPurchaseStrategy)
Defines an interface common to all algorithms. The context class calls the interface to perform the algorithm, identified by the concrete strategy.
-
ConcreteStrategy (VatTaxOrderPurchaseStrategy)
Implements the algorithm using the strategy interface.
-
Context (PurchaseContext)
Holds a dependency on Strategy. Wraps the calls to the concrete strategies, may provide an interface to the strategies to access its data.
Test’s Test Case


4. Fill in the purchase info


The primary goal of the test-related classes’ design is to enable a decoupled and extendable assertion of the different prices on the last page of the purchasing process.
Strategy Design Pattern Java Code
The following class structure will be used.

As you can see, the design of the tests uses the Page Object design pattern heavily. There is nothing unusual in the different pages, so I’m not going to paste every single page’s code here because it is not relevant to the article’s theme. Probably, the most interesting logic is located in the CheckoutPage.
public class CheckoutPage
extends BasePage<CheckoutElements, CheckoutAssertions> {
@Override
protected String getUrl() {
return "http://demos.bellatrix.solutions/checkout/";
}
public void fillBillingInfo(PurchaseInfo purchaseInfo) {
if (purchaseInfo.getCouponCode() != null) {
elements().couponCodeShowInputButton().click();
Driver
.getBrowserWait()
.until(
ExpectedConditions.elementToBeClickable(elements().couponCodeInput())
);
elements().couponCodeInput().sendKeys(purchaseInfo.getCouponCode());
elements().couponCodeApplyButton().click();
}
elements().billingFirstName().sendKeys(purchaseInfo.getFirstName());
elements().billingLastName().sendKeys(purchaseInfo.getLastName());
elements().billingCompany().sendKeys(purchaseInfo.getCompany());
elements().billingCountryWrapper().click();
elements().billingCountryFilter().sendKeys(purchaseInfo.getCountry());
elements().getCountryOptionByName(purchaseInfo.getCountry()).click();
elements().billingAddress1().sendKeys(purchaseInfo.getAddress1());
elements().billingAddress2().sendKeys(purchaseInfo.getAddress2());
elements().billingCity().sendKeys(purchaseInfo.getCity());
elements().billingZip().sendKeys(purchaseInfo.getZip());
elements().billingPhone().sendKeys(purchaseInfo.getPhone());
elements().billingEmail().sendKeys(purchaseInfo.getEmail());
if (purchaseInfo.getShouldCreateAccount()) {
elements().createAccountCheckBox().click();
}
if (purchaseInfo.getShouldCheckPayment()) {
elements().checkPaymentsRadioButton().click();
}
Driver.waitForAjax();
Driver
.getBrowserWait()
.until(
ExpectedConditions.elementToBeClickable(elements().placeOrderButton())
);
Driver
.getBrowserWait()
.until(
ExpectedConditions.invisibilityOfElementLocated(
By.xpath("//div[@class='blockUI blockOverlay']")
)
);
elements().placeOrderButton().click();
}
}
The PurchaseInfo class holds the data about the client’s purchase. Most of the data is populated through string properties.
public class PurchaseInfo {
private String firstName;
private String lastName;
private String company;
private String country;
private String address1;
private String address2;
private String city;
private String zip;
private String phone;
private String email;
private Boolean shouldCreateAccount = false;
private Boolean shouldCheckPayment = false;
private String couponCode = null;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getCompany() {
return company;
}
public void setCompany(String company) {
this.company = company;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getAddress1() {
return address1;
}
public void setAddress1(String address1) {
this.address1 = address1;
}
public String getAddress2() {
return address2;
}
public void setAddress2(String address2) {
this.address2 = address2;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getZip() {
return zip;
}
public void setZip(String zip) {
this.zip = zip;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Boolean getShouldCreateAccount() {
return shouldCreateAccount;
}
public void setShouldCreateAccount(Boolean shouldCreateAccount) {
this.shouldCreateAccount = shouldCreateAccount;
}
public Boolean getShouldCheckPayment() {
return shouldCheckPayment;
}
public void setShouldCheckPayment(Boolean shouldCheckPayment) {
this.shouldCheckPayment = shouldCheckPayment;
}
public String getCouponCode() {
return couponCode;
}
public void setCouponCode(String couponCode) {
this.couponCode = couponCode;
}
}
Implementation without Strategy Design Pattern
The use cases can be automated easily via the usage of the Facade Design Pattern.
public class PurchaseFacade {
private final ItemPage itemPage;
private final ShoppingCartPage shoppingCartPage;
private final CheckoutPage checkoutPage;
public PurchaseFacade(
ItemPage itempage,
ShoppingCartPage shoppingCartPage,
CheckoutPage checkoutPage
) {
this.itemPage = itempage;
this.shoppingCartPage = shoppingCartPage;
this.checkoutPage = checkoutPage;
}
public void purchaseItemNoTax(
String itemUrl,
double itemPrice,
PurchaseInfo purchaseInfo
) {
purchaseItemInternal(itemUrl, itemPrice, purchaseInfo);
checkoutPage.assertVatTax(0);
}
public void purchaseItemVatTax(
String itemUrl,
double itemPrice,
double vatTax,
PurchaseInfo purchaseInfo
) {
purchaseItemInternal(itemUrl, itemPrice, purchaseInfo);
checkoutPage.assertVatTax(vatTax);
}
public void purchaseItemCouponCode(
String itemUrl,
double itemPrice,
double discount,
PurchaseInfo purchaseInfo
) {
purchaseItemInternal(itemUrl, itemPrice, purchaseInfo);
checkoutPage.assertDiscount(discount);
}
private void purchaseItemInternal(
String itemUrl,
double itemPrice,
PurchaseInfo purchaseInfo
) {
itemPage.navigate(itemUrl);
itemPage.assertPrice(itemPrice);
itemPage.clickBuyNowButton();
itemPage.clickViewShoppingCartButton();
shoppingCartPage.clickProceedToCheckoutButton();
shoppingCartPage.assertSubtotalAmount(itemPrice);
checkoutPage.fillBillingInfo(purchaseInfo);
checkoutPage.assertSubtotal(itemPrice);
}
}
The main drawback of such a solution is the number of various methods for the different tax verification cases. If there is a need to introduce a new tax assertion, a new function should be added, which will break the Open Close Principle.
Strategy Design Pattern Implementation
There are hundreds of test cases that you can automate in this sample e-commerce module. Some of the most important are related to the prices’ correctness on the last page of the purchasing process. So the primary goal of the assertions in the tests will be to test if the correct prices are displayed. There are a couple of things that can modify the price – VAT, discounts from coupon codes, etc. The page objects can be combined in the PurchaseContext class to perform a new purchase. The strategy design pattern can be applied to pass the specific validation strategy.
The following interface can define the primary method of the validation strategy.
public interface OrderPurchaseStrategy {
void assertOrderSummary(double itemPrice, PurchaseInfo purchaseInfo);
}
The PurchaseContext class is almost identical to the already discussed facade classes; the only difference is that it holds a dependency on the OrderPurchaseStrategy.
The concrete validation strategy is passed to its constructor.
public class PurchaseContext {
private final OrderPurchaseStrategy orderPurchaseStrategy;
private final ItemPage itemPage;
private final ShoppingCartPage shoppingCartPage;
private final CheckoutPage checkoutPage;
public PurchaseContext(OrderPurchaseStrategy orderPurchaseStrategy) {
this.orderPurchaseStrategy = orderPurchaseStrategy;
itemPage = new ItemPage();
shoppingCartPage = new ShoppingCartPage();
checkoutPage = new CheckoutPage();
}
public void purchaseItem(
String itemUrl,
double itemPrice,
PurchaseInfo purchaseInfo
) {
itemPage.navigate(itemUrl);
itemPage.clickBuyNowButton();
itemPage.clickViewShoppingCartButton();
shoppingCartPage.clickProceedToCheckoutButton();
checkoutPage.fillBillingInfo(purchaseInfo);
orderPurchaseStrategy.assertOrderSummary(itemPrice, purchaseInfo);
}
}
Note
In most cases, I believe that the best approach to assert the tax prices and similar money amounts is to call the real production web services used to power the actual e-commerce module. They should be already entirely tested via unit and integration tests, and their output should be guaranteed.
The primary goal of the E2E tests is to ensure that the correct prices are visualized on the page rather than to check the real calculation logic. So in my concrete implementation of the VatTaxOrderPurchaseStrategy, I have created a dummy sample implementation. In actual tests, instead of calculating the taxes manually, we should call the production web service. If the module doesn’t use web services, you can always create your test web-service and call the production code in wrapper methods. I had already done that for an old legacy module that I had to validate automatically.
public class VatTaxOrderPurchaseStrategy implements OrderPurchaseStrategy {
private final VatTaxCalculationService vatTaxCalculationService;
public VatTaxOrderPurchaseStrategy() {
vatTaxCalculationService = new VatTaxCalculationService();
}
@Override
public void assertOrderSummary(double itemPrice, PurchaseInfo purchaseInfo) {
var currentCountry = Arrays
.stream(Country.values())
.filter(country -> country.toString().equals(purchaseInfo.getCountry()))
.toArray(Country[]::new)[0];
var vatTax = vatTaxCalculationService.calculate(
itemPrice,
currentCountry,
purchaseInfo
);
var checkoutPage = new CheckoutPage();
Driver.waitForAjax();
Driver.waitUntilPageLoadsCompletely();
checkoutPage.assertions().assertOrderVatTaxPrice(vatTax);
}
}
I have implemented a similar logic for CouponCodeOrderPurchaseStrategy.
public class CouponCodeOrderPurchaseStrategy implements OrderPurchaseStrategy {
private final CouponCodeCalculationService couponCodeCalculationService;
public CouponCodeOrderPurchaseStrategy() {
couponCodeCalculationService = new CouponCodeCalculationService();
}
@Override
public void assertOrderSummary(double itemPrice, PurchaseInfo purchaseInfo) {
var discount = couponCodeCalculationService.calculate(
itemPrice,
purchaseInfo.getCouponCode()
);
var checkoutPage = new CheckoutPage();
Driver.waitForAjax();
Driver.waitUntilPageLoadsCompletely();
checkoutPage.assertions().assertOrderDiscountPrice(discount);
}
}
One of the essential benefits of using the Strategy Design Pattern is that if a new tax is introduced in the application, you don’t have to modify your existing pages or the PurchaseContext. You will need only to create a new concrete strategy.
For example, if Online Store announces that starting from tomorrow, a supermodel can deliver you the desired items directly to your door, only for 100 bucks. You can handle the new tax validation by creating a new SuperModelOrderPurchaseStrategy.
Tests Using Strategy Design Pattern
public class StorePurchaseStrategyTests {
@BeforeMethod
public void testInit() {
Driver.startBrowser();
}
@AfterMethod
public void testCleanup() {
Driver.stopBrowser();
}
@Test
public void totalPriceCalculatedCorrect_when_AtCheckoutAndStrategyPatternUsed() {
var itemUrl = "falcon-9";
var itemPrice = 50.00;
var purchaseInfo = new PurchaseInfo();
purchaseInfo.setEmail("info@berlinspaceflowers.com");
purchaseInfo.setFirstName("Anton");
purchaseInfo.setLastName("Angelov");
purchaseInfo.setCompany("Space Flowers");
purchaseInfo.setCountry("Germany");
purchaseInfo.setAddress1("1 Willi Brandt Avenue Tiergarten");
purchaseInfo.setAddress2("Lützowplatz 17");
purchaseInfo.setCity("Berlin");
purchaseInfo.setZip("10115");
purchaseInfo.setPhone("+491888999281");
new PurchaseContext(new VatTaxOrderPurchaseStrategy())
.purchaseItem(itemUrl, itemPrice, purchaseInfo);
}
}
The use of the Strategy Design Pattern to create automated tests is easy. The only thing you have to do is pass the desired algorithm to the newly created PurchaseContext.
Summary
As a consequence of these actions applied through the Strategy Design Pattern’s use, your tests’ architecture will follow the Open-Closed Principle, which states that the code should be open for extension but closed for modification.
For more detailed overview and usage of many more design patterns and best practices in automated testing, check my book “Design Patterns for High-Quality Automated Tests, Java Edition, Clean Code for Bulletproof Tests”. (+ with the book you will get an access to more than 20000+ lines of real-world code examples and video explanations to solidify your knowledge)
