vitest-dev / vitest

Next generation testing framework powered by Vite.
https://vitest.dev
MIT License
12.79k stars 1.15k forks source link

Ability to "unset" an environment variable with stubEnv (or a new function) #5787

Open dietrich-iti opened 4 months ago

dietrich-iti commented 4 months ago

Clear and concise description of the problem

As a developer using Vitest, I want to be able to use the environment variable stubbing feature to make a given variable be "unset", so that I can easily test my code's behavior when environment variables are not present.

Suggested solution

Expand the functionality of vi.stubEnv(name: string, value: string), changing the second argument to value: string | undefined. If undefined is given as the value, remove the named variable from process.env.

Alternative

Alternatively, a new function like vi.stubEnvUnset(name: string) that does the same thing.

Additional context

My current workaround is to use beforeAll to store the process.env entries in question and then delete them, and use afterAll to restore them.

Validations

sheremet-va commented 4 months ago

There is already one: https://vitest.dev/api/vi.html#vi-unstuballenvs

dietrich-iti commented 4 months ago

There is already one: https://vitest.dev/api/vi.html#vi-unstuballenvs

No, sorry, what I am requesting is different than that. unstubAllEnvs undoes any stubbing that has occurred, restoring the environment back to its "true" state.

What I am wanting is:

I've put together an explicit example:

import { expect, test, vi } from 'vitest'

const defEnv = 'ACTUALLY_DEFINED_IN_ENVIRONMENT'  // this is set to "original_defined_value" before vitest starts
const undefEnv = 'ACTUALLY_UNDEFINED_IN_ENVIRONMENT'  // this envvar does not exist when vitest starts

test('this shows the use case for unstubAllEnvs', () => {
  // env starts as: 
  // ACTUALLY_DEFINED_IN_ENVIRONMENT=original_defined_value
  expect(process.env[defEnv]).toEqual('original_defined_value')
  expect(process.env[undefEnv]).toBeUndefined()

  // env becomes:
  // ACTUALLY_DEFINED_IN_ENVIRONMENT=change_defined_value
  // ACTUALLY_UNDEFINED_IN_ENVIRONMENT=add_undefined_value
  vi.stubEnv(defEnv, 'change_defined_value')
  vi.stubEnv(undefEnv, 'add_undefined_value')

  expect(process.env[defEnv]).toEqual('change_defined_value')
  expect(process.env[undefEnv]).toEqual('add_undefined_value')

  // env returns to:
  // ACTUALLY_DEFINED_IN_ENVIRONMENT=original_defined_value
  vi.unstubAllEnvs()

  expect(process.env[defEnv]).toEqual('original_defined_value')
  expect(process.env[undefEnv]).toBeUndefined()
})

test('this shows the use case for the requested functionality', () => {
  // env starts as: 
  // ACTUALLY_DEFINED_IN_ENVIRONMENT=original_defined_value
  expect(process.env[defEnv]).toEqual('original_defined_value')
  expect(process.env[undefEnv]).toBeUndefined()

  // env becomes:
  // ACTUALLY_UNDEFINED_IN_ENVIRONMENT=add_undefined_value
  vi.stubEnv(defEnv, undefined)  // <---------- this is the requested functionality
  vi.stubEnv(undefEnv, 'add_undefined_value')

  expect(process.env[defEnv]).toBeUndefined() // <---------- this is the desired outcome
  expect(process.env[undefEnv]).toEqual('add_undefined_value')

  // env returns to:
  // ACTUALLY_DEFINED_IN_ENVIRONMENT=original_defined_value
  vi.unstubAllEnvs()

  expect(process.env[defEnv]).toEqual('original_defined_value')
  expect(process.env[undefEnv]).toBeUndefined()
})

(note: This code will execute currently, but that's just because in the line labeled "this is the requested functionality", the undefined gets coerced to a string. Consequently, the line labeled "this is the desired outcome" fails with AssertionError: expected 'undefined' to be undefined.)

dietrich-iti commented 4 months ago

I've successfully implemented and tested this feature in a local branch. I took the "alternative" approach and made a new method, as I realized that changing the existing behavior of stubEnv should be avoided if possible. Would it be appropriate to push this branch up at this point, so it can be looked over? Forgive my ignorance -- I haven't contributed to an open-source project before. 😅

    stubEnvUndefined(name) {
      if (!_stubsEnv.has(name))
        _stubsEnv.set(name, process.env[name])
      delete process.env[name]
      return utils
    },
JSanchezIO commented 3 months ago

I'm also a big fan of setting this up. It's quite common to have tests around the absence of an environment variable. However, it's also normal to have these environment variables set when doing local development with something like .env.local. Since the variables are set the aforementioned tests will fail, which can lead to confusion. Expanding the vi.stubEnv utility to support undefined is something I've done in past projects using pnpm's patch command which also allows me to continue using vi.unstubAllEnvs. I know this is still pending triage, but I do have this wired up in a fork as a potential reference point in case this ends up becoming desired functionality.

programmer24601 commented 2 months ago

Shouldn't we call any new method that unsets a specific stubbed environment variable vi.unstubEnv() ? This would match the existing naming scheme: vi.stubEnv(), vi.unstubAllEnvs().

Jtcruthers commented 2 months ago

I agree this functionality would be great. We use feature flags and have hit bugs in the past where an envvar is undefined in an environment and behaves differently than we'd expect. Being able to test and control the functionality of the app when the envvar is undefined is valuable.