Skip to content

Commit

Permalink
Use ldClient configuration instead of bootstrap and update docs.
Browse files Browse the repository at this point in the history
  • Loading branch information
JasonLin0991 committed Jan 26, 2024
1 parent 0099770 commit 0a2aa8b
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 47 deletions.
67 changes: 45 additions & 22 deletions docs/technical-design/launch-darkly-testing-approach.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,40 +23,60 @@ export LD_SDK_KEY='Place Launch Darkly SDK key here'
## Feature flag unit testing

### Client side unit testing
Client side unit testing utilizes the `LDProvider` in our [renderWithProviders](../../services/app-web/src/testHelpers/jestHelpers.tsx) function to set up feature flags. We use this method for testing because the official documented [unit testing](https://docs.launchdarkly.com/guides/sdk/unit-tests/?q=unit+test) method by LaunchDarkly does not work with our implementation. Previously we had used `jest.spyOn` to intercept `useLDClient` and mock the `useLDClient.variation()` function with our defined feature flag values. With `launchdarkly-react-client-sdk@3.0.10` that method did not work anymore.
Client side unit testing utilizes the `LDProvider`, from `launchdarkly-react-client-sdk`, in our [renderWithProviders](../../services/app-web/src/testHelpers/jestHelpers.tsx) function to set up feature flags for each of the tests. Currently, LaunchDarkly does not provide an actual mock `LDProvider` so if or when they do, then we could update this with that provider.

Currently, LaunchDarkly does not provide an actual mock `LDProvider` so if or when they do, then we could update this with that provider.
We use this method for testing because the official documented [unit testing](https://docs.launchdarkly.com/guides/sdk/unit-tests/?q=unit+test) method by LaunchDarkly does not work with our LaunchDarkly implementation. Our implementation follow exactly the documentation, so it could be how we are setting up our unit tests. Previously we had used `jest.spyOn` to intercept `useLDClient` and mock the `useLDClient.variation()` function with our defined feature flag values. With `launchdarkly-react-client-sdk@3.0.10` that method did not work anymore.

#### Configuration
This method tries to "mock" the `LDProvider` for testing by giving it our predefined flags at initialization so that our tests can be individually configured with feature flags values. This implementation needs to be independent of what the values are set at LaunchDarkly to isolate our tests from any changes from values in LaunchDarkly. Therefore, it is configured to fail when trying to make an API call to LaunchDarkly to retrieve feature flag values.
When using the `LDProvider` we need to pass in a mocked `ldClient` in the configuration. This allows us to initialize `ldClient` outside of the provider, which would have required the provider to perform an API call to LaunchDarkly. Now tha this API call does not happen it isolates our unit tests from the feature flag values on the LaunchDarkly server and only use the values we define in each test.

The configuration below, in `renderWithProviders`, the `ldClient` field is how we initialize `ldClient` with our defined flag values. We are using the `ldClientMock()` function to generate a mock that matches the type this field requires.

You will also see that, compared to our configuration in [app-web/src/index.tsx](../../services/app-web/src/index.tsx), the config needed to connect to LaunchDarkly is replaced with `test-url`.

To configure the provider to initialize with our predefined flag values. The configuration below, in `renderWithProviders`, the `bootstrap` field is how we set the initial flag values. You will also see that, compared to our configuration in [app-web/src/index.tsx](../../services/app-web/src/index.tsx), the config needed to connect to LaunchDarkly is missing, this is how we stop the provider from successfully getting a response from LaunchDarkly.
```typescript
const ldProviderConfig: ProviderConfig = {
clientSideID: '',
clientSideID: 'test-url',
options: {
bootstrap: flags,
baseUrl: 'test-url',
streamUrl: 'test-url',
eventsUrl: 'test-url',
},
ldClient: ldClientMock(flags)
}
```

Because we are not fully configuring `LDProvider` it will fail when making an API call to LaunchDarkly. When this happens, there will be console errors and warnings when you run a test. To get around this, there are two `jest.sypOn` functions in `renderWithProviders` to silence all LaunchDarkly errors.

```typescript
jest.spyOn(global.console, 'warn').mockImplementationOnce((message) => {
if (!message.includes('LaunchDarkly')) {
global.console.warn(message);
}
});
The two important functions in the `ldCientMock` are `variation` and `allFlags`. These two functions are the ones we use in the app to get feature flags and here we are mocking them with the flag values we define in each test. If we need any other functions in `ldClient` we would just add the mock to `ldClientMock()`.

jest.spyOn(global.console, 'error').mockImplementationOnce((message) => {
if (!message.includes('LaunchDarkly')) {
global.console.error(message);
}
});
```javascript
const ldClientMock = (featureFlags: FeatureFlagSettings): LDClient => ({
... other functions,
variation: jest.fn((
flag: FeatureFlagLDConstant,
defaultValue: FlagValue | undefined
) => {
if (
featureFlags[flag] === undefined &&
defaultValue === undefined
) {
//ldClient.variation doesn't require a default value, throwing error here if a defaultValue was not provided.
throw new Error(
'ldUseClientSpy returned an invalid value of undefined'
)
}
return featureFlags[flag] === undefined
? defaultValue
: featureFlags[flag]
}),
allFlags: jest.fn(() => {
const defaultFeatureFlags = getDefaultFeatureFlags()
Object.assign(defaultFeatureFlags, featureFlags)
return defaultFeatureFlags
}),
})
```

We define our initial feature flag values, `flags` variable in the config above, by combining the default feature flag values with values passed into `renderWithProviders` for each test. Looking at the code snippet below from `renderWithProviders`, we get the default flag values from [flags.ts](../../services/app-web/src/common-code/featureFlags/flags.ts) using `getDefaultFeatureFlags()` then merge that with `option.featureFlags` values passed into `renderWithProviders`. This will allow each test to configure the specific feature flag values for that test and supply default values for flags the test did not define.
We define our initial feature flag values in the `flags` variable by combining the default feature flag values with values passed into `renderWithProviders` for each test. Looking at the code snippet below from `renderWithProviders`, we get the default flag values from [flags.ts](../../services/app-web/src/common-code/featureFlags/flags.ts) using `getDefaultFeatureFlags()` then merge that with `option.featureFlags` values passed into `renderWithProviders`. This will allow each test to configure the specific feature flag values for that test and supply default values for flags the test did not define.

```typescript
const {
Expand All @@ -74,10 +94,13 @@ const flags = {
}

const ldProviderConfig: ProviderConfig = {
clientSideID: '',
clientSideID: 'test-url',
options: {
bootstrap: flags,
baseUrl: 'test-url',
streamUrl: 'test-url',
eventsUrl: 'test-url',
},
ldClient: ldClientMock(flags)
}
```

Expand Down
79 changes: 54 additions & 25 deletions services/app-web/src/testHelpers/jestHelpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,58 @@ import { S3Provider } from '../contexts/S3Context'
import { testS3Client } from './s3Helpers'
import { S3ClientT } from '../s3'
import {
FeatureFlagLDConstant,
FlagValue,
FeatureFlagSettings,
featureFlagKeys,
featureFlags,
} from '../common-code/featureFlags'
import { LDProvider, ProviderConfig } from 'launchdarkly-react-client-sdk'
import {
LDProvider,
ProviderConfig,
LDClient,
} from 'launchdarkly-react-client-sdk'

function ldClientMock(featureFlags: FeatureFlagSettings): LDClient {
return {
track: jest.fn(),
identify: jest.fn(),
close: jest.fn(),
flush: jest.fn(),
getContext: jest.fn(),
off: jest.fn(),
on: jest.fn(),
setStreaming: jest.fn(),
variationDetail: jest.fn(),
waitForInitialization: jest.fn(),
waitUntilGoalsReady: jest.fn(),
waitUntilReady: jest.fn(),
variation: jest.fn(
(
flag: FeatureFlagLDConstant,
defaultValue: FlagValue | undefined
) => {
if (
featureFlags[flag] === undefined &&
defaultValue === undefined
) {
//ldClient.variation doesn't require a default value, throwing error here if a defaultValue was not provided.
throw new Error(
'ldUseClientSpy returned an invalid value of undefined'
)
}
return featureFlags[flag] === undefined
? defaultValue
: featureFlags[flag]
}
),
allFlags: jest.fn(() => {
const defaultFeatureFlags = getDefaultFeatureFlags()
Object.assign(defaultFeatureFlags, featureFlags)
return defaultFeatureFlags
}),
}
}

/* Render */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
Expand Down Expand Up @@ -51,39 +98,21 @@ const renderWithProviders = (
const s3Client: S3ClientT = s3Provider ?? testS3Client()
const user = userEvent.setup()

const flags = {
const flags: FeatureFlagSettings = {
...getDefaultFeatureFlags(),
...featureFlags,
}

/**
* For unit testing, we do not want to connect to LD to get flag values instead we are initializing the LDProvider
* with a set of flag values that was passed into renderWithProviders. This method will result in console errors and
* warnings, but are suppressed in this helper.
*
* The way LaunchDarkly implements unit tests is not working for our app, so this is the work-around
* https://docs.launchdarkly.com/guides/sdk/unit-tests/?q=unit+test
*/
const ldProviderConfig: ProviderConfig = {
clientSideID: '',
clientSideID: 'test-url',
options: {
bootstrap: flags,
baseUrl: 'test-url',
streamUrl: 'test-url',
eventsUrl: 'test-url',
},
ldClient: ldClientMock(flags),
}

// Ignoring LaunchDarkly errors and warnings from unit testing due to not fully configuring to connect to LaunchDarkly.
jest.spyOn(global.console, 'warn').mockImplementationOnce((message) => {
if (!message.includes('LaunchDarkly')) {
global.console.warn(message)
}
})

jest.spyOn(global.console, 'error').mockImplementationOnce((message) => {
if (!message.includes('LaunchDarkly')) {
global.console.error(message)
}
})

const renderResult = render(
<LDProvider {...ldProviderConfig}>
<MockedProvider {...apolloProvider}>
Expand Down

0 comments on commit 0a2aa8b

Please sign in to comment.