Using factories for testable services in the front-end

Posted on
Cover image for Using factories for testable services in the front-end

If you’ve spent a good chunk of the last decade doing front-end development, a trend to notice — and dispute — is the muddying of responsibilities across component-based architectures. If anything, React hooks might have made things worse. While Angular remains less popular in my city — Berlin —, it does seem to offer a more professional community with higher standards.

To a certain extent, I’m fairly embarassed by how the React and Vue projects I’ve worked in lack that level of professionalism. To be clear, I’m to blame as well. Although highly debatable, Angular projects seem to scale better and offer more reliability. The fact that that community relies on injectors and providers means they’re streets ahead1. Yes, you can get automatic dependency injection in a React project, but it’s something else you have to set-up, and we seem to lack a standard library.

I’ve decided to write a series of small posts detailing how we could achieve cleaner, more testable architectures. The first step I usually take is to abstract business logic away from components, relying on plain and simple JavaScript instead of React hooks and the like. As we’ll see later, we can rely on the event-driven nature of our language to write good Publisher/Subscriber mechanics, eventually arriving at something more sustainable2 than what we currently have.

Laying the foundations: a service for localStorage

Regardless of where you stand on TDD, relying on it to write a predictable class can be a very good thing. But before any of that, I’m getting started as follows:

$ npx create-react-app --typescript testable-localstorage-service

$ cd testable-localstorage-service

$ npm test

At this point, you’ve got the test suite running in watch mode, in this case also whilst using TypeScript. (It’s 2020, we shouldn’t be writing untyped code.) The next step for me is to set this project up in WebStorm, my IDE of choice. You could reasonably do the same without create-react-app, but in this case the lazyness helps us get going faster.

We’re going to get rid of all the React bits for now, as we really only need TypeScript and Jest. Next, we’ll create a folder for our services, and our first one, LocalStorage. My resulting src folder looks like this:

src/
    react-app-env.d.ts
    services/
        LocalStorage/
            index.ts
            LocalStorage.test.ts
            storage.mock.ts

The basics of this service are:

  • We write a class to abstract window.localStorage away.
  • We set up a factory to instantiate that class.
  • By default, our factory returns a new instance of the class using window.localStorage as the default storage provider.

The way the Angular documentation describes this might be helpful:

The Factory recipe adds the following abilities:

  • ability to use other services (have dependencies)
  • service initialization
  • delayed/lazy initialization

The Factory recipe constructs a new service using a function with zero or more arguments (these are dependencies on other services). The return value of this function is the service instance created by this recipe.

Note: All services in AngularJS are singletons. That means that the injector uses each recipe at most once to create the object. The injector then caches the reference for all future needs.

Source

I can see how some of the words in that quote might be controversial in the front-end community. For example, I was rejected in a job application because I used a singleton3 in some part of their technical challenge. It seems developers working with JavaScript don’t take well to that. We also seem to be rather lacking in the injection and instantiation control departments. Oh well…

What all of that is actually going to mean is this:

// services/LocalStorage/index.ts

function LocalStorageServiceFactory(mockStorage?: Storage) {
    return new LocalStorageService(mockStorage || window.localStorage)
}

export default LocalStorageServiceFactory

class LocalStorageService {}

The Storage type in this factory’s mockStorage parameter is defined in TypeScript’s lib.dom.d.ts, as an interface for the Web Storage API.

Also note that the LocalStorageFactory could be written in a number of ways. For example, for production-grade code you could double check that window and window.localStorage are even available in the first place.

In this particular example, providing a mockStorage won’t be the simplest thing in the world. Furthermore, we’re testing something that we can trust is going to work. After all, browsers tend to have very robust implementations of that Storage interface. If window.localStorage is available, you can more or less take it as a given that it works properly. Finally, it could be that your abstraction over something so elementary could actually induce more bugs, and create a false sense of security even with 100% test coverage.

In any case, we’re going to have a bit of fun with this one, and it does serve as an example.

Mocking Storage

Inside our first test file, we can start mocking the storage system our factory is going to be tested with. Remember that if you don’t give it any parameters, it’s going to simple default to window.localStorage. As that one isn’t as easy to test (you’d want to mock that global), we’re going to write a basic storage service:

// services/LocalStorage/storage.mock.ts

export default class MockStorage implements Storage {
    private items: { [key: string]: any }

    constructor() {
        this.items = {}
    }

    get length() {
        return Object.entries(this.items).length
    }

    getItem(key: string) {
        return this.items[key]
    }

    setItem(key: string, value: any) {
        this.items[key] = value
    }

    removeItem(key: string) {
        delete this.items[key]
    }

    clear() {
        this.items = {}
    }

    key(n: number): string|null {
        if (n > this.length) return null
        return Object.keys(this.items)[n]
    }
}

What you see here, is a very crude class that implements all that’s needed in the Storage interface. As it is, it should be valid and compileable TypeScript. If you’ve ever interacted with localStorage, those methods should be fairly familiar too.

With our mock in place, we’re ready to write our first tests.

Testing the factory

Back to our test file, we’re going to write our first couple of tests. As it stands, your IDE might/will complain about missing properties (thanks to TypeScript’s safeguards) and your tests will fail. That’s all good.

// services/LocalStorage/LocalStorage.test.ts

import LocalStorageService from './'
import MockStorage from './storage.mock'

test('initializes properly when given a storage provider', () => {
    const service = LocalStorageService(new MockStorage())
    expect(service).toBeDefined()
    expect(service.hasStorageProvider).toEqual(true)
})

test('initializes with window.localStorage if not given a storage provider', () => {
    const service = LocalStorageService()
    expect(service).toBeDefined()
    expect(service.hasStorageProvider).toEqual(true)
})

Both tests should fail as the hasStorageProvider property is going to be undefined for now. However, note that the service instance itself passes that first assertion, as it is properly defined.

To fix this, we’re heading back to our LocalStorageService class and taking care of that missing property:

// services/LocalStorage/index.ts

(...)

class LocalStorageService {
    private readonly storageProvider: Storage

    constructor(storageProvider: Storage) {
        this.storageProvider = storageProvider
    }

    get hasStorageProvider(): boolean {
        return Boolean(this.storageProvider) // I don't like using !!
    }
}

With the above in place, your tests should now be passing:

PASS  src/services/LocalStorage/LocalStorage.test.ts
  ✓ initializes properly when given a storage provider (1ms)
  ✓ initializes with window.localStorage if not given a storage provider (1ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total

Wrapping up

Given my intention is to showcase how a factory can be used to nicely test a service, I’m not going to place a lot of emphasis on how we’re actually going to test this thing. Instead, you can always refer to the GitHub repository for the source code.

As you can see, the abstraction of localStorage was never the important bit. What I find very helpful, is that I can now test a storage service without mounting and/or mocking any React code. If you depended on Redux for any of this functionality, you know how that can also be complex to mock. The same goes for a situation in which all of this is contained by a HOC, with a router and so on, all now making tests harder to write.

Later in the series we’ll see how using all these concepts will make for easy dependency injection, resulting in an app that’s easier to test and maintain. More importantly, I find that relying on patterns long embraced by other communities helps front-end developers come across more professional. And if nothing else, we might just end up with more sustainable architectural practices, better tests, and more reliable decoupling practices.


  1. Streets ahead on Urban Dictionary
  2. When describing code as sustainable, I’m using that to describe code that is robust, maintainable, and that scales well.
  3. A singleton wraps the instantiation of a class, so that only 1 instance is every instantiated. It’s a common software engineering pattern, but don’t fret if you’ve never heard of it.
Filipe CatraiaWritten by