Global methods

Loading "Global Methods"
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
}
  1. Store the original value (e.g. a function).
  2. Create as spy that has 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 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.
πŸ‘¨β€πŸ’Ό Rewrite test suite to use the vi.spyOn() API from Vitest and spy on the global console.log method. Adjust the setup phase and the assertions to reflect the changes. Verify your solution by running npm test.