Untitled

You know how when you sweep a room, you're gathering dust into one corner of the room, so you can manage it easily?

Like there's nothing stopping you from iteratively moving dust into the dustpan for each square foot of the room, if you're so inclined? It’s just a lot, lot harder to do with a broom.

It's the same way we manage unpredictability in software code where unpredictability is the dust we are trying to sweep, and making dependencies explicit is the act of sweeping unpredictability into a corner so it can be easily managed.

Unpredictability and chaos has the bitter enemy of humanity since man encountered nature and every surprise usually meant doom unless it could be foreseen. Because software is ultimately built for humans, predictability is indeed an important aspect of software quality. You want your software to be predictable, so if a function returns “1” today, it should continue to return "1" on a leap year, blue moon, or during the 13th lunar month.

Writing testable code helps ensure that the behavior of your software is consistent and predictable, and it is the goal of this article, to show how understanding dependency trees and making dependencies explicit, can be harnessed to write testable software. We explore the concepts of explicit and implicit dependencies with examples, and show the problems caused by implicit dependencies.

Introduction to Dependency Trees

When writing testable software, it helps to think of all functions/classes as a dependency tree. Every function may have from zero to many dependencies, with each dependency potentially having their own sub-dependencies, and so on.

Dependency-tree-2020-05-17-1033.png

Here is a getFullName function, showing its dependency tree.

function getFullName(
  firstName: string, // dependency 1
  lastName: string // dependency 2
);

getFullName relies on both firstName and lastName to do its work, so they are its dependencies.

Functions with Explicit Dependencies ✅

In the above example, both dependencies of getFullName are explicitly defined as its parameters. This is ideal because we can pass arguments for these parameters into our function when we want to test it. Knowing that getFullName has no other dependencies means we will always get the same result. For example:

getFullName("John", "Doe") // will always return "John Doe"
getFullName("Sarah", "Doe") // will always return "Sarah Doe"

When dependencies are explicitly defined as parameters of a function, they are known as explicit dependencies. Such a function has no side effects, and is known as a pure function.

Functions with Implicit Dependencies 👎🏽

To explain what side effects are, we will consider the alternative in the example of getFullName(userId).

Untitled

async function getFullName(
  userId: string
) {
  const user = await getUserDetails(
    userId
  )
  return `${user.firstName} ${user.lastName}`
}

Here, getFullName has userId as an explicit dependency, but it also depends on getUserDetails which is not explicitly named.

This kind of dependency is called an implicit dependency, and it introduces unpredictability and side effects in your functions, leading to untestable software. E.g.

getFullName("user-001") // returns "John Doe" today 
// but may return "Sarah Doe", when `getUserDetails` resolves with a different user
// this is a side effect of using `getFullName`

To manage this unpredictability when testing, we can explicitly name getUserDetails as a dependency of getFullName.

Untitled

async function getFullName(
  userId: string,
	getUserDetails: (userId: string) => Promise<{ firstName: string, lastName: string }>
) {
  const user = await getUserDetails()
  return `${user.firstName} ${user.lastName}`
}

Now, when testing, we can have:

getFullName("user-001", async () => ({ firstName: "John", lastName: "Doe" }))
// will always return "John Doe"
getFullName("user-002", async () => ({ firstName: "Sarah", lastName: "Doe" }))
// will always return "Sarah Doe"