Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Null Object Design Pattern in Automated Testing

DZone's Guide to

Null Object Design Pattern in Automated Testing

Checking for null can add to code bloat. An alternative is to provide an object that does nothing. This can also simplify automated unit tests.

· DevOps Zone
Free Resource

“Automated Testing: The Glue That Holds DevOps Together” to learn about the key role automated testing plays in a DevOps workflow, brought to you in partnership with Sauce Labs.

If you are a  regular reader of Automate The Planet you have most probably read some of my articles about Design Patterns in Automated Testing. The newest article from the famous series is dedicated to the Null Object Design Pattern. I am going to explain how to use the pattern to create a default behavior for your strategies and achieve cleaner and more concise test code. Less branching in the code means lower complexity.

In object-oriented computer programming, a Null Object is an object with no referenced value or with defined neutral ("null") behavior. The Null Object Design Pattern describes the uses of such objects and their behavior (or lack thereof).

  • Rid program logic of null checks where possible
  • Provide a non-functional object in place of a null reference
  • Allow methods to be called on Null objects, unlike a null reference

UML Class Diagram

 Null Object Design Pattern Class Diagram

Participants

The classes and objects participating in the Null Object Design Pattern are:

  • IPurchasePromotionalCodeStrategy - Defines the interface for all strategies.
  • UiPurchasePromotionalCodeStrategy - The strategy that is responsible for applying and asserting promotional codes through the UI of the application.
  • NullPurchasePromotionalCodeStrategy - The null implementation of the strategy interface, provides the default implementation when no promotional code is applied.

Null Object Design Pattern C# Code

Test Case

As in most of the examples from the series, we are going to automate a shopping cart process, the Amazon one in particular. I am not going to explain the whole process; you can check the full explanations in my articles dedicated to the Strategy Design Pattern. The page that is interesting for us is the last page from the purchase process: Place Order Page. There you can apply promotional codes and gift cards.

The main idea is that sometimes we need to add promotional codes and then assert that the correct amounts are displayed or saved in the DB. As you can assume, there are various ways to accomplish that. One way is to use the UI directly and assert the text is present in the labels. Another way is to use a direct access to the DB and insert the promotional code, then assert the calculated entries saved in some of the DB's tables. Also, you can achieve the same thing using a web service. I think you get the point—there are multiple solutions to the problem. In order to be able to understand fully the explanations below, I assume that you have read about the Strategy Design Pattern. If you haven't, I suggest you do so. I think that one of the best solutions for the previously stated problem is to use the strategy design pattern.  

Null Object Design Pattern and Strategy Design Pattern The context holds a dependency to IStrategy and wraps the calls to the concrete strategies. In our case, the purchasing workflow is placed in the PurchaseContext class. We call the PurchaseItem method to perform a purchase. The PurchaseContext has a dependency on IPurchasePromotionalCodeStrategy. So depending on what we want to test, we can use the UI to apply and assert the promotional codes or use a direct DB access for a faster test execution.

IPurchasePromotionalCodeStrategy Interface

The interface contains only three methods. One that applies the code, one that asserts the code, and the last one that gets the discount amount.

PurchaseContext Implementation Without Null Object Design Pattern

This is how looks the PurchaseContext code if we don't use the Null Object Design Pattern:

public class PurchaseContextNoNullObjects
{
    private readonly IPurchasePromotionalCodeStrategy purchasePromotionalCodeStrategy;
    private readonly ItemPage itemPage;
    private readonly PreviewShoppingCartPage previewShoppingCartPage;
    private readonly SignInPage signInPage;
    private readonly ShippingAddressPage shippingAddressPage;
    private readonly ShippingPaymentPage shippingPaymentPage;
    private readonly PlaceOrderPage placeOrderPage;

    public PurchaseContextNoNullObjects(
        IPurchasePromotionalCodeStrategy purchasePromotionalCodeStrategy,
        ItemPage itemPage, 
        PreviewShoppingCartPage previewShoppingCartPage, 
        SignInPage signInPage, 
        ShippingAddressPage shippingAddressPage,
        ShippingPaymentPage shippingPaymentPage,
        PlaceOrderPage placeOrderPage)
    {
        this.purchasePromotionalCodeStrategy = purchasePromotionalCodeStrategy;
        this.itemPage = itemPage;
        this.previewShoppingCartPage = previewShoppingCartPage;
        this.signInPage = signInPage;
        this.shippingAddressPage = shippingAddressPage;
        this.shippingPaymentPage = shippingPaymentPage;
        this.placeOrderPage = placeOrderPage;
    }

    public void PurchaseItem(
        string itemUrl, 
        string itemPrice, 
        ClientLoginInfo clientLoginInfo, 
        ClientPurchaseInfo clientPurchaseInfo)
    {
        this.itemPage.Navigate(itemUrl);
        this.itemPage.ClickBuyNowButton();
        this.previewShoppingCartPage.ClickProceedToCheckoutButton();
        this.signInPage.Login(clientLoginInfo.Email, clientLoginInfo.Password);
        this.shippingAddressPage.FillShippingInfo(clientPurchaseInfo);
        this.shippingAddressPage.ClickDifferentBillingCheckBox(clientPurchaseInfo);
        this.shippingAddressPage.ClickContinueButton();
        this.shippingPaymentPage.ClickBottomContinueButton();
        this.shippingAddressPage.FillBillingInfo(clientPurchaseInfo);
        this.shippingAddressPage.ClickContinueButton();
        this.shippingPaymentPage.ClickTopContinueButton();
        double couponDiscount = 0;
        if (purchasePromotionalCodeStrategy != null)
        {
            this.purchasePromotionalCodeStrategy.AssertPromotionalCodeDiscount();
            couponDiscount = 
            this.purchasePromotionalCodeStrategy.GetPromotionalCodeDiscountAmount();
        }
        double totalPrice = double.Parse(itemPrice);
        this.placeOrderPage.AssertOrderTotalPrice(totalPrice, couponDiscount);
        // Some other actions...
        if (purchasePromotionalCodeStrategy != null)
        {
            this.purchasePromotionalCodeStrategy.AssertPromotionalCodeDiscount();
        }          
    }
}

As I already have mentioned, in the body of the PurchaseItem method you can find the order's completion workflow. Though a constructor injection, we pass all dependencies of the PurchaseContext, such as all required pages and the concrete implementation of the IPurchasePromotionalCodeStrategy. However, there might be cases where we don't need to apply promotional codes and the strategy might not be initialized. Because of that we use null checks. If we don't use them a NullReferenceException might be thrown. 

Nuclear Weapons Test

UI Implementation of IPurchasePromotionalCodeStrategy

This is how the UI implementation of the IPurchasePromotionalCodeStrategy interface looks.

public class UiPurchasePromotionalCodeStrategy : IPurchasePromotionalCodeStrategy
{
    private readonly PlaceOrderPage placeOrderPage;
    private readonly double couponDiscountAmount;

    public UiPurchasePromotionalCodeStrategy(
        PlaceOrderPage placeOrderPage,
    double couponDiscountAmount)
    {
        this.placeOrderPage = placeOrderPage;
        this.couponDiscountAmount = couponDiscountAmount;
    }

    public void AssertPromotionalCodeDiscount()
    {
        Assert.AreEqual(
            this.couponDiscountAmount.ToString(), 
        this.placeOrderPage.PromotionalDiscountPrice.Text);
    }

    public double GetPromotionalCodeDiscountAmount()
    {
        return this.couponDiscountAmount;
    }

    public void ApplyPromotionalCode(string couponCode)
    {
        this.placeOrderPage.PromotionalCode.SendKeys(couponCode);
    }
}

This concrete implementation of IPurchasePromotionalCodeStrategy is dependent to the PlaceOrderPage. We use the UI to apply and assert the promotional codes.

Null Object Implementation of IPurchasePromotionalCodeStrategy

The default implementation of the promotional codes' interface is pretty simple.

public class NullPurchasePromotionalCodeStrategy : IPurchasePromotionalCodeStrategy
{
    public void AssertPromotionalCodeDiscount()
    {
    }

    public double GetPromotionalCodeDiscountAmount()
    {
        return 0;
    }

    public void ApplyPromotionalCode(string couponCode)
    {
    }
}

We return zero for the discount amount and the body of the rest of the methods is empty.

PurchaseContext Implementation With Null Object Design Pattern

public class PurchaseContext
{
    private readonly IPurchasePromotionalCodeStrategy purchasePromotionalCodeStrategy;
    private readonly ItemPage itemPage;
    private readonly PreviewShoppingCartPage previewShoppingCartPage;
    private readonly SignInPage signInPage;
    private readonly ShippingAddressPage shippingAddressPage;
    private readonly ShippingPaymentPage shippingPaymentPage;
    private readonly PlaceOrderPage placeOrderPage;

    public PurchaseContext(
        IPurchasePromotionalCodeStrategy purchasePromotionalCodeStrategy,
        ItemPage itemPage, 
        PreviewShoppingCartPage previewShoppingCartPage, 
        SignInPage signInPage, 
        ShippingAddressPage shippingAddressPage,
        ShippingPaymentPage shippingPaymentPage,
        PlaceOrderPage placeOrderPage)
    {
        this.purchasePromotionalCodeStrategy = purchasePromotionalCodeStrategy;
        this.itemPage = itemPage;
        this.previewShoppingCartPage = previewShoppingCartPage;
        this.signInPage = signInPage;
        this.shippingAddressPage = shippingAddressPage;
        this.shippingPaymentPage = shippingPaymentPage;
        this.placeOrderPage = placeOrderPage;
    }

    public void PurchaseItem(
    string itemUrl,
    string itemPrice, 
    ClientLoginInfo clientLoginInfo, 
    ClientPurchaseInfo clientPurchaseInfo)
    {
        this.itemPage.Navigate(itemUrl);
        this.itemPage.ClickBuyNowButton();
        this.previewShoppingCartPage.ClickProceedToCheckoutButton();
        this.signInPage.Login(clientLoginInfo.Email, clientLoginInfo.Password);
        this.shippingAddressPage.FillShippingInfo(clientPurchaseInfo);
        this.shippingAddressPage.ClickDifferentBillingCheckBox(clientPurchaseInfo);
        this.shippingAddressPage.ClickContinueButton();
        this.shippingPaymentPage.ClickBottomContinueButton();
        this.shippingAddressPage.FillBillingInfo(clientPurchaseInfo);
        this.shippingAddressPage.ClickContinueButton();
        this.shippingPaymentPage.ClickTopContinueButton();
        this.purchasePromotionalCodeStrategy.AssertPromotionalCodeDiscount();
        var couponDiscount = 
        this.purchasePromotionalCodeStrategy.GetPromotionalCodeDiscountAmount();
        double totalPrice = double.Parse(itemPrice);
        this.placeOrderPage.AssertOrderTotalPrice(totalPrice, couponDiscount);
        this.purchasePromotionalCodeStrategy.AssertPromotionalCodeDiscount();
    }
}

As you have most probably noticed the code is almost identical to the previous implementation with the small difference that the null checks are missing.

Null Object Design Pattern is about giving a default implementation for filling the absence of an object and it is not about avoiding null reference exceptions. If you see Null Object Design Pattern implemented with no object collaboration then there is something wrong in the way the pattern is implemented.

Space Telescope Test

Null Object Design Pattern in Tests

[TestClass]
public class AmazonPurchaseNullObjectTests
{ 
    [TestInitialize]
    public void SetupTest()
    {
        Driver.StartBrowser();
    }

    [TestCleanup]
    public void TeardownTest()
    {
        Driver.StopBrowser();
    }

    [TestMethod]
    public void Purchase_SeleniumTestingToolsCookbook()
    {
        string itemUrl = "/Selenium-Testing-Cookbook-Gundecha-Unmesh/dp/1849515743";
        string itemPrice = "40.49";
        ClientPurchaseInfo clientPurchaseInfo = new ClientPurchaseInfo(
            new ClientAddressInfo()
            {
                FullName = "John Smith",
                Country = "United States",
                Address1 = "950 Avenue of the Americas",
                State = "New York",
                City = "New York City",
                Zip = "10001-2121",
                Phone = "00164644885569"
            });
        clientPurchaseInfo.CouponCode = "99PERDIS";
        ClientLoginInfo clientLoginInfo = new ClientLoginInfo()
        {
            Email = "g3984159@trbvm.com",
            Password = "ASDFG_12345"
        };

        var purchaseContext = new PurchaseContext(
            new NullPurchasePromotionalCodeStrategy(),
            new ItemPage(Driver.Browser),
            new PreviewShoppingCartPage(Driver.Browser),
            new SignInPage(Driver.Browser),
            new ShippingAddressPage(Driver.Browser),
            new ShippingPaymentPage(Driver.Browser),
            new PlaceOrderPage(Driver.Browser));

        ////var purchaseContext = new PurchaseContext(
        ////new UiPurchasePromotionalCodeStrategy(
        ////new PlaceOrderPage(Driver.Browser), 20),
        //// new ItemPage(Driver.Browser),
        //// new PreviewShoppingCartPage(Driver.Browser),
        //// new SignInPage(Driver.Browser),
        //// new ShippingAddressPage(Driver.Browser),
        //// new ShippingPaymentPage(Driver.Browser),
        //// new PlaceOrderPage(Driver.Browser));

        purchaseContext.PurchaseItem(
            itemUrl,
            itemPrice, 
            clientLoginInfo, 
            clientPurchaseInfo);
    }
}

The usage of the promotional code's strategies in tests is straightforward. If you want to apply a promotional code, you should use the UiPurchasePromotionalCodeStrategy, or if you don't, use NullPurchasePromotionalCodeStrategy (where the default do-nothing behavior is wrapped). 

In the next part of the series I am going to show you how to utilize the Null Object Design Pattern to the maximum extent through the Singleton Design Pattern and Unity IOC Container.

Learn about the importance of automated testing as part of a healthy DevOps practice, brought to you in partnership with Sauce Labs.

Topics:
null object pattern ,automated testing

Published at DZone with permission of Anton Angelov, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}