Software testing SOLID Principles in Test Automation

Examples of SOLID Principles in Test Automation

Introduction

As the demand for high-quality software continues to increase, the role of SDET (Software Development Engineer in Test) has become increasingly crucial. SDETs are tasked with ensuring that the software being developed meets the necessary quality standards, and test automation plays a vital role in achieving this objective. However, as test automation code expands in size and complexity, it becomes susceptible to issues like code duplication, tight coupling, and fragile test suites. This is where the SOLID principles come into play.

Exploring the SOLID Principles

The SOLID principles, coined by Robert C. Martin, comprise a set of five design principles aimed at enhancing the understandability, flexibility, and maintainability of software designs. These principles are:

  • S – Single Responsibility Principle
  • O – Open/Closed Principle
  • L – Liskov Substitution Principle
  • I – Interface Segregation Principle
  • D – Dependency Inversion Principle

In the realm of test automation, comprehending and applying these principles can significantly enhance the quality and manageability of test code.

1. Single Responsibility Principle (SRP):

A class should have only one reason to change. In the context of test automation, avoid adding everything in one class, method, or folder. Instead, make each entity, method, or class responsible for a specific logic.

Some examples of how you can apply it:

  • Page Object Model :
    • Create a distinct class for each page, such as HomePage, LoginPage, OrdersPage, etc. Instead of consolidating all page locators into a single “PageObject” class, distribute them among individual classes responsible for their respective pages.
    • For larger pages, consider breaking them down into smaller PageFragments, such as header, table, footer, etc.
  • Each functionality should have a separate test class:
    • This ensures that each test class is dedicated to testing a specific feature or functionality.
    • For instance, a login test class should concentrate solely on testing the login functionality and should not be tasked with testing unrelated features such as user registration.
// Single Responsibility Principle (SRP)
// Example: Creating separate test classes for each feature or user story
public class LoginTest
{
    [Test]
    public void UserCanLoginSuccessfully()
    {
        // Test logic for login functionality
    }
}

public class ShoppingCartTest
{
    [Test]
    public void UserCanAddItemToCart()
    {
        // Test logic for adding items to the shopping cart
    }
}

public class CheckoutTest
{
    [Test]
    public void UserCanCheckoutSuccessfully()
    {
        // Test logic for checkout functionality
    }
}
  • Custom WebdriverFactory class :  We can customize each of the WebDriver implementations and add extensions for specific webdrivers.
public class WebDriverFactory
    {
        private IWebDriver Driver;

        public IWebDriver InitDriver(AppiumLocalService appiumLocalService, EnvironmentConfig environmentConfig)
        {
            switch (platformName)
            {
                case PlatformName.Web:
                    switch (browserName)
                    {
                        case BrowserName.FireFox:
                            //Add Firefox browser Options
                            break;

                        case BrowserName.Chrome:
                            //Add Chrome browser Options
                            break;
                    }
                    break;
            }
            return Driver;
        }  
   }
  • Helper classes :  
    • You should avoid creating a helper class that encompasses all the current methods. Instead, consider dividing these classes logically.
    • For example, having  different helper classes for FileHelpers, DatabaseHelpers, CsvHelpers, JsonHelpers etc.
  • One method should be responsible for only one action
    • When the method calculates a value, it should focus solely on performing the calculation without also reading that value from a file, database, or other external source. Any additional actions should be handled in a separate class or method. Remember to consider the true purpose of the entity when creating a class or method, and avoid overloading it with unrelated logic.

2. Open/Closed Principle (OCP):

Software entities (modules, classes, functions, etc.) should be open for extension but closed for modification. In test automation, this principle encourages us to design our test frameworks in a way that allows us to add new tests or features without modifying existing code. For instance, we can achieve this by using polymorphism and inheritance to create reusable test components.

Some examples of how you can apply it:

  • API Data Objects : 
    • Today, we have implemented the REST API Endpoint for version 1 (v1). A Register data object class has been created for this version.
    • Six months later, a new version of the endpoint was introduced with additional logic and new fields added to the Register feature.
    • Rather than updating the existing class, a new class can be created based on the current one. This approach enables us to utilize the existing class fields/properties and incorporate new properties in the new class.
public class RegisterApiV1
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public string Photo { get; set; }
        public string UId { get; set; }
       
    }

public class RegisterApiV2 : RegisterApiV1
    {
        public DateTime DateOfBirth { get; set; }
  
    }
  • BaseTest class implementation: 
    • Let’s consider having a base class that most test classes use. In the future, you may want to add specific logic relevant only to certain test classes. For example, creating a base class for account functionality to retrieve User Data from a Json file based on the user type provided.
//UIBaseTest
public class UIBaseTest
    {

        [AssemblyInitialize]
        public static void Init(TestContext testContext)
        {
            //Assembly Initialization code
        }

        [TestInitialize]
        public void Setup()
        {
            //SetUp code
        }

        [TestCleanup]
        public void TearDown()
        {
           //TearDown code
        }

    }

// Account BaseTest
public class AccountBaseTest : UIBaseTest
    {

        protected static Login GetUserData(string userType)
        {
            //Method to get userdata from json
        }

    }

3. Liskov Substitution Principle (LSP):

Objects of a superclass should be replaceable with objects of its subclasses without affecting the program’s correctness. In test automation, this principle emphasises the importance of writing tests that are independent of the implementation details of the system under test. By adhering to this principle, we can ensure that our tests remain robust and unaffected by changes to the underlying codebase.

For Example : Writing tests using high-level abstractions (e.g., interfaces or abstract classes) rather than concrete implementations, allowing us to swap out implementations without affecting the tests.

// Liskov Substitution Principle (LSP)
// Example: Writing tests using high-level abstractions
public interface ILoginPage
{
    void EnterCredentials(string username, string password);
    void ClickLoginButton();
}

public class LoginPage : ILoginPage
{
    public void EnterCredentials(string username, string password)
    {
        // Implementation for entering credentials
    }

    public void ClickLoginButton()
    {
        // Implementation for clicking login button
    }
}

public class LoginTest
{
    [Test]
    public void UserCanLoginSuccessfully()
    {
        ILoginPage loginPage = new LoginPage();
        loginPage.EnterCredentials("username", "password");
        loginPage.ClickLoginButton();
        // Test assertion...
    }
}

Applying the Liskov Substitution Principle in our test automation solution helps ensure that our tests are maintainable, extensible, and reusable. This is achieved through the use of polymorphism to create tests that are adaptable and generic, making them suitable for changes in our application.

4. Interface Segregation Principle (ISP):

Clients should not be forced to depend on interfaces they do not use. In the context of test automation, this principle encourages us to design clear and concise interfaces for our test components. This helps in creating modular and maintainable test suites, where each test only depends on the interfaces it requires, rather than being tightly coupled to the entire system.

For Example: Defining small, focused interfaces for test actions (e.g., ILoginPage, IShoppingCartPage) to ensure that tests only depend on the methods they need.

// Interface Segregation Principle (ISP)
// Example: Defining small, focused interfaces for test actions
public interface IShoppingCartPage
{
    void AddItemToCart(string item);
    void RemoveItemFromCart(string item);
}

public class ShoppingCartPage : IShoppingCartPage
{
    public void AddItemToCart(string item)
    {
        // Implementation for adding item to cart
    }

    public void RemoveItemFromCart(string item)
    {
        // Implementation for removing item from cart
    }
}

public class ShoppingCartTest
{
    [Test]
    public void UserCanAddItemToCart()
    {
        IShoppingCartPage shoppingCartPage = new ShoppingCartPage();
        shoppingCartPage.AddItemToCart("Product A");
        // Test assertion...
    }
}

By applying the Interface Segregation Principle in our test automation solutions, we can make our code more extensible, modular, and maintainable. This approach allows us to create interfaces suited to various web element types, avoiding unnecessary dependencies on methods not relevant to certain web elements.

5. Dependency Inversion Principle (DIP):

High-level modules should not depend on low-level modules. Both should depend on abstractions. In test automation, this principle advocates for decoupling our tests from external dependencies such as databases, APIs, or UI elements. By using dependency injection and abstraction layers, we can create testable components that are independent of specific implementations, making our tests more resilient to changes.

For Example: Using dependency injection frameworks or creating mock objects to isolate tests from external dependencies such as databases or APIs.

// Dependency Inversion Principle (DIP)
// Example: Using dependency injection frameworks or creating mock objects
public interface IDatabase
{
    void SaveData(string data);
}

public class Database : IDatabase
{
    public void SaveData(string data)
    {
        // Implementation for saving data to the database
    }
}

public class MockDatabase : IDatabase
{
    public void SaveData(string data)
    {
        // Mock implementation for testing
    }
}

public class DataProcessor
{
    private readonly IDatabase _database;

    public DataProcessor(IDatabase database)
    {
        _database = database;
    }

    public void ProcessData(string data)
    {
        // Process data...
        _database.SaveData(data);
    }
}

public class DataProcessorTest
{
    [Test]
    public void DataIsSavedToDatabase()
    {
        IDatabase mockDatabase = new MockDatabase();
        DataProcessor dataProcessor = new DataProcessor(mockDatabase);

        dataProcessor.ProcessData("Test data");

        // Test assertion...
    }
}

Benefits of Implementing SOLID Principles in Test Automation:

Embracing SOLID principles aids software developers in avoiding common design issues and creating agile products. It strengthens code, enhances readability, maintainability, and testability. Implementing these principles in test automation:

  • Reduces dependencies, enhancing modularity, testability, and maintainability.
  • Enhances code understanding, extension, and scalability for swift adaptation to business needs.
  • Improves design and code quality, preventing a single change from breaking the entire application.
  • Boosts test coverage and simplifies debugging processes.
  • Decreases maintenance workload, enabling quick and effortless code refactoring.

Conclusion

By applying the SOLID principles to test automation, SDETs can create more maintainable, flexible, and scalable test frameworks. These principles guide us in designing test code that is easier to understand, extend, and maintain, ultimately leading to higher-quality software products. As SDETs, it’s essential to continuously strive for excellence in our craft, and leveraging the SOLID principles is one step towards achieving that goal.

Witness how our meticulous approach and cutting-edge solutions elevated quality and performance to new heights. Begin your journey into the world of software testing excellence. To know more refer to Tools & Technologies & QA Services.

If you would like to learn more about the awesome services we provide, be sure to reach out.

Happy Testing 🙂