Global methods
Loading "Global Methods"
Run locally for transcripts
Often, global behaviors you would want to mock will be encapsulated to particular global objects. For example, the
log()
method is a part of the console
object, which means you don't have to mock the entire console
just to know when it prints something.In that regard, mocking global methods is no different than mocking or spying on regular functions!
In fact, it's identical. It's always
vi.spyOn()
. The only thing that changes is the target object we provide to that utility:// Spying on a method of the object you own.
vi.spyOn(myObj, 'method')
// Spying on a method of the object you don't own.
vi.spyOn(console, 'log')
Since we are touching on the same utility yet again, let's take a look at how
vi.spyOn()
actually works. Here's a simplified implementation from Vitest:const kState = Symbol('kState')
function spyOn<T extends Object>(target: T, method: keyof T) {
// Store the original function.
const original = target[method]
const spy = function (...args: Parameters<T[keyof T]>) {
// Get or create a state inside the spy.
const state = original[kState] || {}
// Keep track of the spy's calls.
state.called = true
state.calls++
state.calls.push(args)
// Store the state on the spy
// so our test can reference its values.
Object.defineProperty(original, kState, {
value: state,
})
// Execute the original function.
return original.apply(this, args)
}
// Reassign the original function with the spy.
target[method] = spy
}
- Store the original value (e.g. a function).
- Create as
spy
that has internalstate
to keep track of the function's calls. - Replace the original function with the spy.
You know what this reminds me of? Monkey patching.
π¦ Monkey patching is a technique to change or extend the behavior of a function or a method at runtime. It's often used to add logging, debugging, or testing capabilities to the existing code.
On its own, monkey-patching globals is rather π dangerous and you shouldn't be using it directly. Doing things like
console.log = vi.fn()
in test can quickly lead to verbose test setups and brittle tests.Instead, relying on the tools that your testing framework provides is the way to go. They are battle-tested and are implemented with both yours and your test environment's best interests in mind.
Your task
You have a helper function called
printServerUrl()
that is meant to print any provided URL to the console:interface Server {
host: string
port?: number
}
/**
* Print the server URL to the console.
*/
export function printServerUrl(server: Server) {
const url = new URL(`http://${server.host}`)
url.port = server.port?.toString() || ''
console.log(`Server is listening at ${url.href}`)
}
Since the very intention behind this function concerns itself around
console.log
, you would have to gain control over that global method to spy on its calls and make sure that the correct URL is indeed being printed out.