Understanding how to write testable code is one of the biggest frustrations I had when I finished school and started working at my first real-world job.

Today, while working on a chapter in solidbook.io, breaking down some code and picking apart everything wrong with it, I realized that several principles govern how I write code to be testable.

In this article, I want to present you with a straightforward methodology you can apply to both front-end and back-end code for how to write testable code.

Prerequisite readings

You may want to read the following pieces beforehand. 😇

Dependencies are relationships

You may already know this, but the first thing to understand is that when we import or even mention the name of another class, function, or variable from one class (let's call this the source class), whatever was mentioned becomes a dependency to the source class .

In the dependency inversion & injection article, we looked at an example of a UserController that needed access to a UserRepo to get all users.

controllers/userController.ts

import { UserRepo } from '../repos' class UserController { private userRepo : UserRepo ; constructor ( ) { this . userRepo = new UserRepo ( ) ; } async handleGetUsers ( req , res ) : Promise < void > { const users = await this . userRepo . getUsers ( ) ; return res . status ( 200 ) . json ( { users } ) ; } }

The problem with this approach was that when we do this, we create a hard source-code dependency.

The relationship looks like the following:

This means that if we ever wanted to test UserController , we'd need to bring UserRepo along for the ride as well. The thing about UserRepo , though, is that it also brings a whole damn database connection with it as well. And that's no good.

If we need to spin up a database to run unit tests, that makes all our unit tests slow.

Ultimately, we can fix this by using dependency inversion, putting an abstraction between the two dependencies.

Abstractions that can invert the flow of dependencies are either interfaces or abstract classes.

This works by placing an abstraction (interface or abstract class) in between the dependency you want to import and the source class. The source class imports the abstraction, and remains testable because we can pass in anything that has adhered to the contract of the abstraction, even if it's a mock object.

controllers/userController.ts

import { IUserRepo } from '../repos' class UserController { private userRepo : IUserRepo ; constructor ( userRepo : IUserRepo ) { this . userRepo = userRepo ; } async handleGetUsers ( req , res ) : Promise < void > { const users = await this . userRepo . getUsers ( ) ; return res . status ( 200 ) . json ( { users } ) ; } }

In our scenario with UserController , it now refers to an IUserRepo interface (which costs nothing) rather than referring to the potentially heavy UserRepo that carries a db connection with it everywhere it goes.

If we wish to test the controller, we can satisfy the UserController 's need for an IUserRepo by substituting our db-backed UserRepo for an in-memory implementation. We can create one like this:

class InMemoryMockUserRepo implements IUserRepo { ... }

The methodology

Here's my thought process for keeping code testable. It all starts when you want to create a relationship from one class to another.

Start: You want to import or mention the name of a class from another file.

Question: do you care about being able to write tests against the source class in the future?

If no, go ahead and import whatever it is because it doesn't matter.

If yes, consider the following restrictions. You may depend on the class only if it is at least one of these:

The dependency is an abstraction (interface or abstract class).

The dependency is from the same layer or an inner layer (see The Dependency Rule).

It is a stable dependency.

If at least one of these conditions passes, import the dependency- otherwise, don't.

Importing the dependency introduces the possibility that it will be hard to test that component in the future.

Again, you can fix scenarios where the dependency breaks one of those rules by using Dependency Inversion.

Front-end example (React w/ TypeScript)

What about front-end development?

The same rules apply!

Take this React component (pre-hooks) involving a container component (inner layer concern) that depends on a ProfileService (outer layer - infra).

containers/ProfileContainer.tsx

import * as React from 'react' import { ProfileService } from './services' ; import { IProfileData } from './models' interface ProfileContainerProps { } interface ProfileContainerState { profileData : IProfileData | { } ; } export class ProfileContainer extends React . Component < ProfileContainerProps , ProfileContainerState > { private profileService : ProfileService ; constructor ( props : ProfileContainerProps ) { super ( props ) ; this . state = { profileData : { } } this . profileService = new ProfileService ( ) ; } async componentDidMount ( ) { try { const profileData : IProfileData = await this . profileService . getProfile ( ) ; this . setState ( { ... this . state , profileData } ) } catch ( err ) { alert ( "Ooops" ) } } render ( ) { return ( < div > Im a profile container </ div > ) } }

If ProfileService is something that makes network calls to a RESTful API, there's no way for us to test ProfileContainer and prevent it from making real API calls.

We can fix this by doing two things:

1. Putting an interface in between the ProfileService and ProfileContainer

First, we create the abstraction and then ensure that ProfileService implements it.

services/index.tsx

import { IProfileData } from "../models" ; export interface IProfileService { getProfile : ( ) => Promise < IProfileData > ; } export class ProfileService implements IProfileService { async getProfile ( ) : Promise < IProfileData > { ... } }

Then we update ProfileContainer to rely on the abstraction instead.

containers/ProfileContainer.tsx

import * as React from 'react' import { ProfileService , IProfileService } from './services' ; import { IProfileData } from './models' interface ProfileContainerProps { } interface ProfileContainerState { profileData : IProfileData | { } ; } export class ProfileContainer extends React . Component < ProfileContainerProps , ProfileContainerState > { private profileService : IProfileService ; constructor ( props : ProfileContainerProps ) { super ( props ) ; this . state = { profileData : { } } this . profileService = new ProfileService ( ) ; } async componentDidMount ( ) { try { const profileData : IProfileData = await this . profileService . getProfile ( ) ; this . setState ( { ... this . state , profileData } ) } catch ( err ) { alert ( "Ooops" ) } } render ( ) { return ( < div > Im a profile container </ div > ) } }

2. Compose a ProfileContainer with a HOC that contains a valid IProfileService .

Now we can create HOCs that use whatever kind of IProfileService we wish. It could be the one that connects to an API like what follows:

hocs/withProfileService.tsx

import React from "react" ; import { ProfileService } from "../services" ; interface withProfileServiceProps { } function withProfileService ( WrappedComponent : any ) { class HOC extends React . Component < withProfileServiceProps , any > { private profileService : ProfileService ; constructor ( props : withProfileServiceProps ) { super ( props ) ; this . profileService = new ProfileService ( ) ; } render ( ) { return ( < WrappedComponent profileService = { this . profileService } { ... this . props } /> ) ; } } return HOC ; } export default withProfileService ;

Or it could be a mock one that uses an in-memory profile service as well.

hocs/withMockProfileService.tsx

import * as React from "react" ; import { MockProfileService } from "../services" ; interface withProfileServiceProps { } function withProfileService ( WrappedComponent : any ) { class HOC extends React . Component < withProfileServiceProps , any > { private profileService : MockProfileService ; constructor ( props : withProfileServiceProps ) { super ( props ) ; this . profileService = new MockProfileService ( ) ; } render ( ) { return ( < WrappedComponent profileService = { this . profileService } { ... this . props } /> ) ; } } return HOC ; } export default withProfileService ;

For our ProfileContainer to utilize the IProfileService from an HOC, it has to expect to receive an IProfileService as a prop within ProfileContainer rather than being added to the class as an attribute.

containers/ProfileContainer.tsx

import * as React from "react" ; import { IProfileService } from "./services" ; import { IProfileData } from "./models" ; interface ProfileContainerProps { profileService : IProfileService ; } interface ProfileContainerState { profileData : IProfileData | { } ; } export class ProfileContainer extends React . Component < ProfileContainerProps , ProfileContainerState > { constructor ( props : ProfileContainerProps ) { super ( props ) ; this . state = { profileData : { } } ; } async componentDidMount ( ) { try { const profileData : IProfileData = await this . props . profileService . getProfile ( ) ; this . setState ( { ... this . state , profileData } ) ; } catch ( err ) { alert ( "Ooops" ) ; } } render ( ) { return < div > Im a profile container </ div > } }

Finally, we can compose our ProfileContainer with whichever HOC we want- the one containing the real service, or the one containing the fake service for testing.