Skip to content

Commit e9920bc

Browse files
authored
feat: improve flag override API for testing (#548)
1 parent e24250d commit e9920bc

9 files changed

Lines changed: 358 additions & 65 deletions

File tree

.changeset/solid-doodles-glow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@reflag/node-sdk": minor
3+
---
4+
5+
improve flag override API for testing

packages/node-sdk/README.md

Lines changed: 74 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -498,14 +498,32 @@ reflagClient.initialize().then(() => {
498498

499499
## Testing
500500

501-
When writing tests that cover code with flags, you can toggle flags on/off programmatically to test the different behavior.
501+
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.
502502

503503
`reflag.ts`:
504504

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

508-
export const reflag = new ReflagClient();
508+
export const reflag = new ReflagClient({
509+
offline: true,
510+
});
511+
```
512+
513+
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()`:
514+
515+
```typescript
516+
// pass directly in the constructor
517+
const client = new ReflagClient({
518+
offline: true,
519+
flagOverrides: { myFlag: true },
520+
});
521+
522+
// or replace the base overrides at a later time
523+
client.setFlagOverrides({ myFlag: false });
524+
525+
// clear only the base overrides
526+
client.clearFlagOverrides();
509527
```
510528

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

521539
describe("API Tests", () => {
522540
it("should return 200 for the root endpoint", async () => {
523-
reflag.flagOverrides = {
541+
reflag.setFlagOverrides({
524542
"show-todo": true,
525-
};
543+
});
526544

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

534-
See more on flag overrides in the section below.
552+
`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:
553+
554+
```typescript
555+
export const flag = function (name: string, enabled: boolean): void {
556+
let remove: (() => void) | undefined;
557+
558+
beforeEach(function () {
559+
remove = reflagClient.pushFlagOverrides({ [name]: enabled });
560+
});
561+
562+
afterEach(function () {
563+
remove?.();
564+
remove = undefined;
565+
});
566+
};
567+
568+
describe("foo", () => {
569+
describe("with new search ranking enabled", () => {
570+
flag("search-ranking-v2", true);
571+
572+
describe("with summaries enabled", () => {
573+
flag("smart-summaries", true);
574+
575+
// ...
576+
});
577+
});
578+
});
579+
```
580+
581+
The precedence is:
582+
583+
1. Base overrides from the constructor or `setFlagOverrides()`
584+
2. Temporary layers added by `pushFlagOverrides()`
585+
586+
If the same flag is set in both places, the pushed override wins until its remove function is called.
587+
588+
`pushFlagOverrides()` also accepts a function if the temporary override depends on the evaluation context:
589+
590+
```typescript
591+
const remove = client.pushFlagOverrides((context) => ({
592+
"smart-summaries": context.user?.id === "qa-user",
593+
}));
594+
595+
// ...
596+
597+
remove();
598+
```
535599

536600
## Flag Overrides
537601

538-
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:
602+
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.
603+
604+
For automated tests, see the [Testing](#testing) section above.
605+
606+
When testing locally during development, you also have these additional ways to provide overrides:
539607

540608
1. Through environment variables:
541609

@@ -563,20 +631,6 @@ REFLAG_FLAGS_DISABLED=flag3,flag4
563631
}
564632
```
565633

566-
1. Programmatically through the client options:
567-
568-
You can use a simple `Record<string, boolean>` and pass it either in the constructor or by setting `client.flagOverrides`:
569-
570-
```typescript
571-
// pass directly in the constructor
572-
const client = new ReflagClient({ flagOverrides: { myFlag: true } });
573-
// or set on the client at a later time
574-
client.flagOverrides = { myFlag: false };
575-
576-
// clear flag overrides. Same as setting to {}.
577-
client.clearFlagOverrides();
578-
```
579-
580634
To get dynamic overrides, use a function which takes a context and returns a boolean or an object with the shape of `{isEnabled, config}`:
581635

582636
```typescript
Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
11
import request from "supertest";
22
import app, { todos } from "./app";
3-
import { beforeEach, describe, it, expect, beforeAll } from "vitest";
3+
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
44

55
import reflag from "./reflag";
66

7+
function flag(name: string, enabled: boolean): void {
8+
let remove: (() => void) | undefined;
9+
10+
beforeEach(() => {
11+
remove = reflag.pushFlagOverrides({ [name]: enabled });
12+
});
13+
14+
afterEach(() => {
15+
remove?.();
16+
remove = undefined;
17+
});
18+
}
19+
720
beforeAll(async () => await reflag.initialize());
21+
822
beforeEach(() => {
9-
reflag.featureOverrides = {
23+
reflag.setFlagOverrides({
1024
"show-todos": true,
11-
};
25+
});
26+
});
27+
28+
afterEach(() => {
29+
reflag.clearFlagOverrides();
1230
});
1331

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

27-
it("should return no todos when list is disabled", async () => {
28-
reflag.featureOverrides = () => ({
29-
"show-todos": false,
45+
describe("with show-todos temporarily disabled", () => {
46+
flag("show-todos", false);
47+
48+
it("should return no todos", async () => {
49+
const response = await request(app).get("/todos");
50+
expect(response.status).toBe(200);
51+
expect(response.body).toEqual({ todos: [] });
3052
});
31-
const response = await request(app).get("/todos");
32-
expect(response.status).toBe(200);
33-
expect(response.body).toEqual({ todos: [] });
3453
});
3554
});

packages/node-sdk/examples/express/app.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import reflag from "./reflag";
22
import express from "express";
3-
import { BoundReflagClient } from "../src";
3+
import type { BoundReflagClient } from "../../src";
44

55
// Augment the Express types to include the `reflagUser` property on the `res.locals` object
66
// This will allow us to access the ReflagClient instance in our route handlers

packages/node-sdk/examples/express/bucket.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { ReflagClient, Context, FlagOverrides } from "../../";
1+
import { ReflagClient, Context, FlagOverrides } from "../../src";
22

33
type CreateConfigPayload = {
44
minimumLength: number;
55
};
66

77
// Extending the Flags interface to define the available features
8-
declare module "../../types" {
8+
declare module "../../src/types" {
99
interface Flags {
1010
"show-todos": boolean;
1111
"create-todos": {
@@ -18,7 +18,7 @@ declare module "../../types" {
1818
}
1919
}
2020

21-
let featureOverrides = (_: Context): FlagOverrides => {
21+
const flagOverrides = (_: Context): FlagOverrides => {
2222
return {
2323
"create-todos": {
2424
isEnabled: true,
@@ -39,5 +39,5 @@ let featureOverrides = (_: Context): FlagOverrides => {
3939
export default new ReflagClient({
4040
// Optional: Set a logger to log debug information, errors, etc.
4141
logger: console,
42-
featureOverrides, // Optional: Set feature overrides
42+
flagOverrides, // Optional: Set flag overrides
4343
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from "./bucket";

0 commit comments

Comments
 (0)