Are you using most recommended and fundamental SOLID principles in your automation?
Like any development work, automation also needs to be planned, designed, developed, maintained and extended over time. Your automation code or automation framework is an application which is testing your actual application. That’s why SOLID principles are highly recommended for your automation to make it easy to understand, use, maintain and extend over time. When SOLID principles are not applied, your automation might become fragile, hard to maintain and hard to extend very soon.
This is the first post in a series of posts that describe & apply the SOLID principles of object-oriented design and programming to web automation using Selenium & Java. Even if you are using a different programming language like C#, Python or automation tool like HP UFT/QTP for automation, you should definitely consider applying SOLID principles to make your automation tester life easy. Please note that we will be discussing about applying SOLID principles to web automation using Selenium & Java. But, once you learn the concepts, you can easily apply them to any object-oriented development. In this post, we will be exploring what SOLID principles stands for.
SOLID principles are the first five object-oriented design principles by Robert C. Martin, popularly known as Uncle Bob. SOLID is an acronym where:-
Let's explore and understand each individual principle. Also, let's find out how these SOLID principles make you a better automation tester.
In other words, to achieve this, a class should only have a single responsibility and it should do that very well. Every class in your automation should only have a single responsibility and that all of its methods should be aligned with that responsibility. Let's try to understand this principle by looking at an example from our daily life. When you are driving a car/bike, you want to fully concentrate on the single responsibility - driving. You don't want to do or concentrate on other tasks like talking on a phone, eating.
We should also have methods in classes that are very specific like loginAs(username, password) to login with given username and password, testSuccessfulLogin(username,password) which tests only one thing that login should be successful with valid username/password.
You should be able to easily add additional functionality for a class without changing its code. OCP says that a class should be open for extension and closed for modification. The "closed" part of the rule states that once a class has been developed and tested, the class code shouldn't change except for any bug fixes. The "open" part of the rule states that you should be able to extend existing code in order to introduce new functionality. We are trying to add new functionality without modifying the existing code/functionality and by adding new classes/code as required. This is very important to minimise the impact of changes and errors from existing code. Let's try to understand this principle by looking at an example from our daily life. Let's say you live in a 2 bedroom house and you are looking for a 3 bedroom house due to reasons like growing children. If you have free/unused space available, it's very easy to extend the house by building another bedroom. Also with this, you are minimise the impact of changes on the existing 2 bedroom house.
Automation Example: Let’s say that you are automating an online store application. You have Customer class to represent store customers and respective related customer actions. Now, your company has introduced VIP customer concept to reward loyal customers with discounts and free delivery. To implement VIP customer behaviour in your automation, OCP suggests that keep the Customer class same without modifying it and create a new VipCustomer class by inheriting from Customer class. Now in the VipCustomer class extend the behaviour as required.
When you pass subtype for a base type argument or when you assign/instantiate base type with subtype, the program/code should work properly without changing its behaviour and shouldn't break. This principle was introduced by and named after Barbara Liskov. Let's try to understand this principle by looking at an example from our daily life. Let's say that you have a wall clock at home or you have a wrist watch. They both need batteries to work. If you buy batteries as per the specifications from any brand like Panasonic, Sony, Duracell, you expect the wall clock or wrist watch to work properly without any issues when powered by those batteries.
Automation Example: Considering the above example of automating an online store application with Customer and VIP Customer categories, let's say there is a method calculateDeliveryCharge(Customer customer, OrderInfo orderInfo) which calculates delivery charge when customer and order information is passed. When we pass Customer object to calculateDeliveryCharge() method with order information, it should return delivery charge. Note that we are providing free delivery to VIP Customers. So, when we pass VipCustomer object for customer argument to calculateDeliveryCharge() method with order information, the program/code should work properly without changing its behaviour and shouldn't throw any exceptions.
It's good to have small role specific interfaces rather than one big general interface. ISP states that clients should never be forced to implement interfaces that they don't use or clients should never be forced to depend on methods that they don't use. When a class depends upon another class, the number of members visible from the another class to the dependent class should be minimised. When you apply the ISP, classes implement multiple smaller role specific interfaces and dependent classes depend on required role specific interfaces for the given task. Let's try to understand this principle by looking at an example from our daily life. When you are travelling in a train and when ticket inspector wants to check your ticket, you will be showing only your ticket and not all your luggage. Similarly, ticket inspector wants to check your ticket only and not any other belongings of you. We should reveal/expose only what's required for the given task.
Automation Example-1: Let's again take the example of automating an online store application. You have to test that an email is received by the customer after successful purchase. Let's say that you have an EmailHelper class with a method isEmailReceived(). Your Customer class might have so many fields and methods like firstName, lastName, emailAddress, phoneNumber, deliveryAddress, billingAddress, getPurchaseHisotry(), getOrdersInProgress(). But isEmailReceived() method only needs to know firstName, lastName, emailAddress fields and doesn't need to now all the other fields/methods of Customer class. Consider implementation like isEmailReceived(Customer customer, string subject, string body): We are exposing whole Customer class in isEmailReceived() method and then one can easily access/change details like phoneNumber, deliveryAddress, billingAddress in isEmailReceived() method. We want to minimize the risk to Customer object by passing only details that are required to the clients/methods. We can achieve this by below implementation by using an Emilable interface which represents a contact that can be emailed. Now, we can further reuse Emailable interface for other types like StoreManager, Supplier to whom your online store sends emails. With this approach, we can use isEmailReceived() method with any implementation that implements Emailable interface and respective input object for recipient argument is safe from any changes/actions. I hope now you have understood the power of ISP.
Automation Example-2: Selenium API has good examples of ISP. Selenium API has a number of very fine grained, role based client specific interfaces like WebDriver, WebElement, Alert. We should favour role based interfaces instead of generic interfaces.
DIP states that Software entities (classes, modules, functions, etc.) should depend on abstractions (like interfaces) and not on concretions (like concrete class types). DIP promotes code to an interface approach. For example, in Selenium automation code, we code to a WebDriver interface variable "driver" whenever we want to work with web browser and the same code works for any browser type like FirefoxDriver, ChromeDriver, InternetExploerDriver which implements the WebDriver interface. DIP mainly suggests below 2 rules:
Let's try to understand this principle by looking at an example from our daily life. When you go to a cash machine/ATM, the cash machine/ATM expects a valid debit/credit card. The machine has a dependency on valid card abstraction and not on specific concrete type cards like only Visa, only Maestro or only issued by specific bank. The machine works for any valid card type implementation and we are providing the card to the machine from outside which provides so much flexibility and easiness to use the machine.
Automation Example: Look at below page object class for a Login Page. We can observe following with respect to DIP from above LoginPage class:
I hope you have got a good understanding of SOLID principles by now. Thanks for reading this post. Any questions or Need Expert Help, contact us.