SOLID Principles for Testers: The Interface Segregation Principle

We’re over halfway done learning about the SOLID principles! Today it’s time to learn about the “I”: the Interface Segregation Principle.

In order to understand this principle, we first need to understand what an interface is. An interface is a definition of a set of methods that can implemented in a class. Each class that implements the interface must use all of the methods included in the interface. Because the interface only defines the method signature (name, parameters, and return type), the methods can vary in each implementation.

The Interface Segregation Principle states that no class should be forced to depend on methods that it does not use. In order to understand this, let’s look at an example. Imagine you have a website to test that has two different forms: an Employee form and an Employer form. So you decide to create a Form interface that has methods for interacting with the various objects that can be found in a form:

interface Form {
    fillInTextField(id: String, value: String): void;
    selectRadioButton(id: String): void;
    checkCheckbox(id: String): void;
}

When you create an EmployeeForm class, you set it to implement the Form interface:

class EmployeeForm implements Form {
    fillInTextField(id: String, value: String): void {
      driver.findElement(By.id(id)).sendKeys(value)
    }
  
    selectRadioButton(id: String): void {
      driver.findElement(By.id(id)).click()
    }
  
    checkCheckbox(id: String): void {
      driver.findElement(By.id(id)).click()
    }
}

This works great, because the Employee form has text fields, radio buttons, and checkboxes.

Next, you create an EmployerForm class, which also implements the Form interface. But this form only has text fields and no radio buttons or checkboxes. So you implement the interface like this:

class EmployerForm implements Form {
    fillInTextField(id: String, value: String): void {
      driver.findElement(By.id(id)).sendKeys(value)
    }
  
    selectRadioButton(id: String): void {
      // no radio button
      throw new Error("No radio button exists");
    }
  
    checkCheckbox(id: String): void {
      // no checkbox
      throw new Error("No checkbox exists");
    }
}

You’ll never call the selectRadioButton and checkCheckbox methods in the EmployerForm class because there are no radio buttons or checkboxes in that form, but you need to create methods for them anyway because of the interface. This violates the Interface Segregation Principle.

So, how can you use interfaces with these forms without violating the principle? You can create separate interfaces for text fields, radio buttons, and checkboxes, like this:

interface TextField {
    fillInTextField(id: String, value: String): void;
}
  
interface RadioButton {
    selectRadioButton(id: String): void;
}
  
interface Checkbox {
    checkCheckbox(id: String): void;
}

Then when you create the EmployeeForm class you can implement the three interfaces like this:

class EmployeeForm implements TextField, RadioButton, Checkbox {
    fillInTextField(id: String, value: String): void {
      driver.findElement(By.id(id)).sendKeys(value)
    }
  
    selectRadioButton(id: String): void {
      driver.findElement(By.id(id)).click()

    }
  
    checkCheckbox(id: String): void {
      driver.findElement(By.id(id)).click()
    }
}

Now when you create the EmployerForm class, you only need to implement the TextField interface:

  class EmployerForm implements TextField {
    fillInTextField(id: String, value: String): void {
      driver.findElement(By.id(id)).sendKeys(value)
    }
  }

When each class only needs to implement the methods they need, classes become easier to maintain. And having smaller interfaces means that they can be used in a greater variety of scenarios. This is the benefit of the Interface Segregation Principle.