SOLID Principles for Testers: The Dependency Inversion Principle

It’s time for the last SOLID principle! The Dependency Inversion Principle has two parts, and we’ll take a look at them one at a time. First, the principle states that “High-level modules should not depend on low-level modules, but should instead depend on abstractions.”

In order to understand this, we first need to know the difference between “high-level modules” and “low-level modules”. A low-level module is something that handles one specific task, such as making a request from a database or sending a file to a printer. For the first example in this post, we’ll use a class called “AddText” that will clear a text field and enter new text into it:

class AddText {
    clearAndEnterText(id: string, value: string) {
        driver.findElement(By.id(id)).clear().sendKeys(value)
    }
}

A high-level module is something that provides core functionality to an application or a test, such as generating a report or creating a new user. In this example, we’ll use a class called “UpdatePerson” that will update a value in a person record:

class UpdatePerson {
    private addText: AddText
    constructor(addText: AddText) {
        this.addText = addText
    }
    update(updateId: string, updateValue: string) {
        this.addText.clearAndEnterText(updateId, updateValue)
    }
}

The way we would update a record in this example is by first initializing an instance of the AddText class, then initializing an instance of the UpdatePerson class, and then calling the update function to make the update:

const addText = new AddText()
const updateUser = new UpdatePerson(addText)
updateUser.update('lastName', 'Smith')

But this example violates the Dependency Inversion Principle! The “UpdatePerson” class is very dependent on the “AddText” class. If the signature (parameters and return type) of the “clearAndEnterText” function changes in the “AddText” class, the “UpdatePerson” class will have to change as well.

So let’s update our code to comply with the principle. Instead of creating an “AddText” class, we’ll create an “AddText” interface:

interface AddText {
  clearAndEnterText(id: string, value, string): void
}

Then we’ll create a class called “PersonForm” that will implement the interface:

class PersonForm implements AddText {
    clearAndEnterText(id: string, value: string) {
        driver.findElement(By.id(id)).clear().sendKeys(value)
    }
}

And finally, we’ll update our UpdatePerson class so that it will use the PersonForm:

class UpdatePerson {
    private form: PersonForm
    constructor(form: PersonForm) {
        this.form = form
    }
    update(updateId: string, updatevalue: string) {
        this.form.clearAndEnterText(updateId, updateValue)
    }
}

Now we can update the person’s value by first creating an instance of the PersonForm class, and then creating and using the UpdatePerson class:

const userForm = new PersonForm()
const updateUser = newUpdatePerson(userForm)
updateUser.update('lastname', 'Smith')

Now both the PersonForm class and the UpdatePerson class depend on the interface instead of a low-level module. If the signature of the “clearAndEnterText” interface changes, we’ll need to update the PersonForm, but we won’t need to make changes to the UpdatePerson class.

The second part of the Dependency Inversion Principle states that “Abstractions should not depend on details; details should depend on abstractions”. An abstraction is an interface or abstract class that defines a set of behaviors without providing specific implementations. Both high-level and low-level modules should depend on abstractions, and if the details of their implementation change, they should not affect the abstraction.

In other words, the PersonForm class can make any changes to the “clearAndEnterText” function, and it will have no effect on the “AddText” interface. For example, we could change the PersonForm class to have a log statement, but that won’t have any impact on the “AddText” interface:

class PersonForm implements AddText {
    clearAndEnterText(id: string, value: string) {
        driver.findElement(By.id(id)).clear().sendKeys(value)
        console.log('Element updated')
    }
}

This concludes my five-post examination of SOLID principles! I’d like to extend a special thank you to my colleague Monica Standifer, who helped me better understand the principles. I definitely learned a lot in this process, and I hope that you have found these simple examples that use methods commonly found in software testing to be helpful!