Typesafe mockable globals in Vue3
Tags: vue, typescript
Published: 2022-04-29 10:42:48

Typically in large apps there are plenty of things that you need global access to throughout the codebase; things like locale, environment, feature flags, settings etc. Then there are also functions that are useful to be globally accessible, like notifications, console actions or formatters. Then - assuming you are working with [typescript](https://www.typescriptlang.org/) - it's nice to have all of them properly typed. And finally - assuming you are writing tests (for example using [jest](https://jestjs.io/) or [vitest](https://vitest.dev/)) - it's nice if all of this can be properly controlled (mocked) in automated tests.

## How do we achieve this?

Let's say my application is called 'Peer'. I will begin by defining an interface that will contain some useful globals; specifically a string that we can use for date formatting and some console actions[^1] :

<sub>**PeerGlobals.ts**</sub>
```ts
export interface PeerGlobals {
  log: (m: string) => void
  logError: (m: string) => void
  defaultDateFormat: string
}
```

Then I will implement and provide it in a plugin:

<sub>**PeerPlugin.ts**</sub>
```ts
import { App, Plugin } from 'vue'
import { PeerGlobals } from 'PeerGlobals'

export const PeerPlugin: Plugin {
  install(app: App) {
    const globals: PeerGlobals = {
      log: console.log,
      logError: console.error,
      defaultDateFormat: 'yyyy-MM-dd',
    }
    app.provide('globals', globals)
  }
}
```

<sub>**main.ts**</sub>
```ts
import { createApp } from 'vue'
import App from './App.vue'
import { PeerPlugin } from './PeerPlugin'

const app = createApp(App)
// use any other plugin here like Router or Pinia
app.use(PeerPlugin)
app.mount('#app')
```

Now in any component we can do this:

<sub>**MyComponent.vue**</sub>
```vue
<script lang="ts" setup>
import type { PeerGlobals } from '@/PeerGlobals'

const globals = inject('globals') as PeerGlobals
</script>
```

As for testing, I will make a file `mockPeerGlobals.ts` which I can then use in any test that mount any components that depend on these globals:

<sub>**mockPeerGlobals.ts**</sub>
```ts
import type { PeerGlobals } from '@/PeerGlobals'

export const mockPeerGlobals: PeerGlobals = {
  log: () => {},
  logError: () => {},
  defaultDateFormat: 'yyyy-MM-dd',
}
```

<sub>**MyComponent.spec.ts**</sub>
```ts
import { mount } from '@vue/test-utils'
import { mockPeerGlobals } from 'mockPeerGlobals'
import MyComponent from '@/components/MyComponent.vue'

function mountMyComponent() {
  return mount(MyComponent, {
    global: {
      provide: {
        globals: mockPeerGlobals
      }
    }
  })
}

// ...tests
```
---
## Assertions on global functions

in `mockPeerGlobals.ts` the log functions are empty stubs, but typically you will want to replace them with mock functions so you can assert that they have been called as expected - (for example using [`jest.fn()`](https://jestjs.io/docs/mock-functions) in jest or [`vi.fn()`](https://vitest.dev/api/#vi-fn) in vitest). Just be sure to properly reset all mocks before running a test.

## Using `window` and `document`

Sometimes we need access to `window` and `document`, which is typically not available within a test environment. Therefore it is useful to also put these behind our global interface. However these objects contain a huge amount of properties, so mocking those will be way too much work. Instead we can use some typescript magic called [mapped types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html) to make all properties optional:

<sub>**PeerGlobals.ts**</sub>
```ts
type MockWindow = {
  [k in keyof Window]?: Window[k]
}

type MockDocument = {
  [k in keyof Document]?: Document[k]
}

export interface PeerGlobals {
  window: (Window & typeof globalThis) | MockWindow
  document: Document | MockDocument
  // ...other globals
}
```

Now in our mock globals we *only* need to implement the functions that are relevant for our tests. Supposing `querySelectorAll` is the only one we are using:

<sub>**mockPeerGlobals.ts**</sub>
```ts
import type { PeerGlobals } from '@/PeerGlobals'

export const mockPeerGlobals: PeerGlobals = {
  window: {},
  document: {
    querySelectorAll: () => []
  },
  // ...other globals
}
```

---

### What if we want mock implementations on a per test basis?

Exporting a mock object as we did in `mockPeerGlobals.ts` is somewhat restrictive: All tests are forced to use the same globals object. But sometimes we need test-specific mock implementations. Let's change `mockPeerGlobals.ts` to support this, where we will use a helper function from the Ramda library; [`mergeDeepRight`](https://ramdajs.com/docs/#mergeDeepRight):

<sub>**mockPeerGlobals.ts**</sub>
```ts
import { mergeDeepRight } from 'ramda'
import type { PeerGlobals } from '@/PeerGlobals'

// ...define default globals

export function getMockPeerGlobals(overrides?: Partial<PeerGlobals>): PeerGlobals {
  return mergeDeepRight(mockPeerGlobals, (overrides as any) || {})
}
```

Now in a test we can override any property on any level of nesting, without affecting the rest of the globals:

<sub>**MyComponent.spec.ts**</sub>
```ts
import { mount } from '@vue/test-utils'
import { mockPeerGlobals } from 'mockPeerGlobals'
import MyComponent from '@/components/MyComponent.vue'

function mountMyComponent() {
  return mount(MyComponent, {
    global: {
      provide: {
        globals: getMockPeerGlobals({
          document: {
            querySelectorAll: () => []
          }
          // the rest of globals remain unaffected
        })
      }
    }
  })
}

// ...tests
```

[^1]: Putting console actions behind an interface is useful for preventing logs being printed in our test output.