Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/solid-doodles-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@reflag/node-sdk": minor
---

improve flag override API for testing
94 changes: 74 additions & 20 deletions packages/node-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -498,14 +498,32 @@ reflagClient.initialize().then(() => {

## Testing

When writing tests that cover code with flags, you can toggle flags on/off programmatically to test the different behavior.
When writing tests that cover code with flags, you can toggle flags on/off programmatically to test different behavior. For tests, you will often want to run the client in offline mode and provide flag overrides directly through the client options.

`reflag.ts`:

```typescript
import { ReflagClient } from "@reflag/node-sdk";

export const reflag = new ReflagClient();
export const reflag = new ReflagClient({
offline: true,
});
```

You can then set base overrides for a test run by passing `flagOverrides` in the constructor, replacing them later with `setFlagOverrides()`, or clearing them with `clearFlagOverrides()`:

```typescript
// pass directly in the constructor
const client = new ReflagClient({
offline: true,
flagOverrides: { myFlag: true },
});

// or replace the base overrides at a later time
client.setFlagOverrides({ myFlag: false });

// clear only the base overrides
client.clearFlagOverrides();
```

`app.test.ts`:
Expand All @@ -520,9 +538,9 @@ afterEach(() => {

describe("API Tests", () => {
it("should return 200 for the root endpoint", async () => {
reflag.flagOverrides = {
reflag.setFlagOverrides({
"show-todo": true,
};
});

const response = await request(app).get("/");
expect(response.status).toBe(200);
Expand All @@ -531,11 +549,61 @@ describe("API Tests", () => {
});
```

See more on flag overrides in the section below.
`pushFlagOverrides()` serves a different purpose: it adds a temporary layer on top of the base overrides and returns a remove function that removes only that layer. This is useful for nested tests:

```typescript
export const flag = function (name: string, enabled: boolean): void {
let remove: (() => void) | undefined;

beforeEach(function () {
remove = reflagClient.pushFlagOverrides({ [name]: enabled });
});

afterEach(function () {
remove?.();
remove = undefined;
});
};

describe("foo", () => {
describe("with new search ranking enabled", () => {
flag("search-ranking-v2", true);

describe("with summaries enabled", () => {
flag("smart-summaries", true);

// ...
});
});
});
```

The precedence is:

1. Base overrides from the constructor or `setFlagOverrides()`
2. Temporary layers added by `pushFlagOverrides()`

If the same flag is set in both places, the pushed override wins until its remove function is called.

`pushFlagOverrides()` also accepts a function if the temporary override depends on the evaluation context:

```typescript
const remove = client.pushFlagOverrides((context) => ({
"smart-summaries": context.user?.id === "qa-user",
}));

// ...

remove();
```

## Flag Overrides

Flag overrides allow you to override flags and their configurations locally. This is particularly useful for development and testing. You can specify overrides in three ways:
Flag overrides allow you to override flags and their configurations locally. This is particularly useful when testing changes locally, for example when running your app and clicking around to verify behavior before deploying your changes.

For automated tests, see the [Testing](#testing) section above.

When testing locally during development, you also have these additional ways to provide overrides:

1. Through environment variables:

Expand Down Expand Up @@ -563,20 +631,6 @@ REFLAG_FLAGS_DISABLED=flag3,flag4
}
```

1. Programmatically through the client options:

You can use a simple `Record<string, boolean>` and pass it either in the constructor or by setting `client.flagOverrides`:

```typescript
// pass directly in the constructor
const client = new ReflagClient({ flagOverrides: { myFlag: true } });
// or set on the client at a later time
client.flagOverrides = { myFlag: false };

// clear flag overrides. Same as setting to {}.
client.clearFlagOverrides();
```

To get dynamic overrides, use a function which takes a context and returns a boolean or an object with the shape of `{isEnabled, config}`:

```typescript
Expand Down
37 changes: 28 additions & 9 deletions packages/node-sdk/examples/express/app.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,32 @@
import request from "supertest";
import app, { todos } from "./app";
import { beforeEach, describe, it, expect, beforeAll } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";

import reflag from "./reflag";

function flag(name: string, enabled: boolean): void {
let remove: (() => void) | undefined;

beforeEach(() => {
remove = reflag.pushFlagOverrides({ [name]: enabled });
});

afterEach(() => {
remove?.();
remove = undefined;
});
}

beforeAll(async () => await reflag.initialize());

beforeEach(() => {
reflag.featureOverrides = {
reflag.setFlagOverrides({
"show-todos": true,
};
});
});

afterEach(() => {
reflag.clearFlagOverrides();
});

describe("API Tests", () => {
Expand All @@ -24,12 +42,13 @@ describe("API Tests", () => {
expect(response.body).toEqual({ todos });
});

it("should return no todos when list is disabled", async () => {
reflag.featureOverrides = () => ({
"show-todos": false,
describe("with show-todos temporarily disabled", () => {
flag("show-todos", false);

it("should return no todos", async () => {
const response = await request(app).get("/todos");
expect(response.status).toBe(200);
expect(response.body).toEqual({ todos: [] });
});
const response = await request(app).get("/todos");
expect(response.status).toBe(200);
expect(response.body).toEqual({ todos: [] });
});
});
2 changes: 1 addition & 1 deletion packages/node-sdk/examples/express/app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import reflag from "./reflag";
import express from "express";
import { BoundReflagClient } from "../src";
import type { BoundReflagClient } from "../../src";

// Augment the Express types to include the `reflagUser` property on the `res.locals` object
// This will allow us to access the ReflagClient instance in our route handlers
Expand Down
8 changes: 4 additions & 4 deletions packages/node-sdk/examples/express/bucket.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { ReflagClient, Context, FlagOverrides } from "../../";
import { ReflagClient, Context, FlagOverrides } from "../../src";

type CreateConfigPayload = {
minimumLength: number;
};

// Extending the Flags interface to define the available features
declare module "../../types" {
declare module "../../src/types" {
interface Flags {
"show-todos": boolean;
"create-todos": {
Expand All @@ -18,7 +18,7 @@ declare module "../../types" {
}
}

let featureOverrides = (_: Context): FlagOverrides => {
const flagOverrides = (_: Context): FlagOverrides => {
return {
"create-todos": {
isEnabled: true,
Expand All @@ -39,5 +39,5 @@ let featureOverrides = (_: Context): FlagOverrides => {
export default new ReflagClient({
// Optional: Set a logger to log debug information, errors, etc.
logger: console,
featureOverrides, // Optional: Set feature overrides
flagOverrides, // Optional: Set flag overrides
});
1 change: 1 addition & 0 deletions packages/node-sdk/examples/express/reflag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./bucket";
Loading
Loading