Global methods

Loading "Global Methods"
Often, the global behaviors you want to mock are encapsulated within specific global objects. For example, the log() method is part of the console object, so you don’t need to mock the entire console just to track when it prints something.
In that regard, mocking global methods is no different from mocking or spying on regular functions!
In fact, it's identicalβ€”it's always vi.spyOn(). The only difference is the target object we provide to the 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
}
  1. Store the original value (e.g. a function).
  2. Create a spy with internal state to keep track of the function's calls.
  3. Replace the original function with the spy.
You know what this reminds me of? Monkey patching.
πŸ¦‰ Monkey patching is a technique for changing or extending the behavior of a function or a method at runtime. It's often used to add logging, debugging, or testing capabilities to existing code.
On its own, monkey-patching globals is rather πŸ“œ dangerous and you shouldn't use it directly. Doing things like console.log = vi.fn() in test can quickly lead to verbose test setups and brittle tests.
Instead, rely on the tools provided by your testing framework. They are battle-tested and designed with both your needs and the stability of your test environment 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.
πŸ‘¨β€πŸ’Ό Rewrite test suite to use the vi.spyOn() API from Vitest and spy on the global console.log method. Adjust the setup phase and assertions to reflect the changes. Verify your solution by running npm test.