From 9d421b342b99600a4a5801e728d38e7bf8c4542b Mon Sep 17 00:00:00 2001 From: Leslie Lau Date: Thu, 4 Dec 2025 15:43:16 -0500 Subject: [PATCH 1/4] Create mock testing page --- docs/sdk/features.md | 1 - docs/sdk/lifecycle.md | 1 - docs/sdk/mock-testing.md | 140 ++++++++++++++++++++++++++++++++ sidebars.js | 18 ++-- src/css/custom.css | 4 + static/sidebar/mock-testing.svg | 1 + 6 files changed, 157 insertions(+), 8 deletions(-) create mode 100644 docs/sdk/mock-testing.md create mode 100644 static/sidebar/mock-testing.svg diff --git a/docs/sdk/features.md b/docs/sdk/features.md index f332219d..e29aa209 100644 --- a/docs/sdk/features.md +++ b/docs/sdk/features.md @@ -1,6 +1,5 @@ --- title: Features and Functionality -sidebar_position: 1 --- DevCycle strives to ensure that all our APIs and SDKs have identical functionality diff --git a/docs/sdk/lifecycle.md b/docs/sdk/lifecycle.md index ec7cd63b..2ddf1b57 100644 --- a/docs/sdk/lifecycle.md +++ b/docs/sdk/lifecycle.md @@ -1,6 +1,5 @@ --- title: SDK Release Lifecycle -sidebar_position: 2 --- # SDK Release Lifecycle diff --git a/docs/sdk/mock-testing.md b/docs/sdk/mock-testing.md new file mode 100644 index 00000000..192d2786 --- /dev/null +++ b/docs/sdk/mock-testing.md @@ -0,0 +1,140 @@ +--- +title: Mock Testing with DevCycle +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Mock testing allows you to test your application's behavior with specific Feature and Variable configurations without making API calls to DevCycle. This enables faster, more reliable tests that don't depend on external services. + +## Methods for Mock Testing + +### DevCycleUser + +``` +mockUser = DevCycleUser( + user_id='test_user', + email='test@example.com', + country='CA', + customData={ + customKey='value' + } +) +``` + +### Variable and VariableValue + +Test retrieving a Variable value through the `.variable()` or `.variableValue()` method. + + + + ```python + def test_variable_string(self, mock_client): + """Test getting a string variable value""" + + mock_variable = Variable( + _id='var_str', + key='example-text', + type='String', + value='step-1' + ) + + mock_client.variable.return_value = mock_variable + + result = mock_client.variable(self.mock_user, 'example-text', 'default') + + # Assertions + self.assertEqual(result.value, 'step-1') + self.assertEqual(result.key, 'example-text') + self.assertEqual(result.type, 'String') + ``` + + + +### AllVariables + +Test retrieving all active Variables through the `allVariable()` or `all_variables()` method. + + + + ```python + def test_all_variables(self, mock_client): + """Test retrieving all variables for a user""" + + mock_variables = { + 'feature-flag-1': Variable( + _id='var1', + key='feature-flag-1', + type='Boolean', + value=True, + eval=EvalReason( + reason='TARGETING_MATCH', + details='ALL USERS', + target_id='targeting_rule1' + ) + ), + 'feature-flag-2': Variable( + _id='var2', + key='feature-flag-2', + type='String', + value='variant-a', + eval=EvalReason( + reason='SPLIT', + details='Random Distribution | Audience Match', + target_id='targeting_rule2' + ) + ), + } + + mock_client.all_variables.return_value = mock_variables + mock_client.is_initialized.return_value = True + + # Call the method + result = mock_client.all_variables(self.mock_user) + + # Assertions + self.assertEqual(len(result), 2) + self.assertTrue(result['feature-flag-1'].value) + self.assertEqual(result['feature-flag-2'].value, 'variant-a') + mock_client.all_variables.assert_called_once_with(self.mock_user) + ``` + + + +## Client-Side SDKs: Bootstrapping + +Client-side Web SDKs (JavaScript, React, Next.js) support bootstrapping, which allows you to pass a pre-configured DevCycle configuration during initialization. + +```javascript +const bootstrapConfig = { + // Your test configuration here +} + +const devcycleClient = initializeDevCycle( + '', + user, + { bootstrapConfig } +) +``` + +## Server-Side SDKs: Local Bucketing + +Server-side SDKs with Local Bucketing mode enabled, will allow you to provide the SDK a local configuration file or mock data. + +```javascript +// Example for Node.js SDK +const devcycleClient = initializeDevCycle( + '', + { + configCDN: 'https://your-test-config.com/config.json', + // or use local file + } +) +``` + +## Best Practices + +- Use consistent test configurations across your test suite +- Test both enabled and disabled states of Features +- Test Variables and Variations to ensure all code paths are covered +- Keep mock configurations close to your test files for easy maintenance \ No newline at end of file diff --git a/sidebars.js b/sidebars.js index de6ac5a3..eee1a795 100644 --- a/sidebars.js +++ b/sidebars.js @@ -55,12 +55,6 @@ module.exports = { className: 'section-title overview', id: 'sdk/index', // The internal path }, - { - type: 'doc', - label: 'SDK Lifecycle', - className: 'section-title lifecycle', - id: 'sdk/lifecycle', - }, { type: 'doc', label: 'SDK Features', @@ -91,6 +85,18 @@ module.exports = { collapsible: true, collapsed: true, }, + { + type: 'doc', + label: 'SDK Mock Testing', + className: 'section-title mock-testing', + id: 'sdk/mock-testing', + }, + { + type: 'doc', + label: 'SDK Lifecycle', + className: 'section-title lifecycle', + id: 'sdk/lifecycle', + }, ], bestPractices: [ { diff --git a/src/css/custom.css b/src/css/custom.css index a85ae3be..0cc56102 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -303,6 +303,10 @@ html[data-theme='dark'] .DocSearch { background-image: url(../../static/sidebar/roadmap.svg); } +.section-title.mock-testing > a::before { + background-image: url(../../static/sidebar/mock-testing.svg); +} + .responsiveCal { position: relative; padding-bottom: 75%; diff --git a/static/sidebar/mock-testing.svg b/static/sidebar/mock-testing.svg new file mode 100644 index 00000000..ac5d91bf --- /dev/null +++ b/static/sidebar/mock-testing.svg @@ -0,0 +1 @@ + \ No newline at end of file From 17a061c4c3e92d6ce174081713603f5a7cf1e13a Mon Sep 17 00:00:00 2001 From: Leslie Lau Date: Tue, 16 Dec 2025 14:03:25 -0500 Subject: [PATCH 2/4] Add Cloud Bucketing content --- docs/sdk/mock-testing.md | 205 +++++++++++++++++++++++---------------- 1 file changed, 121 insertions(+), 84 deletions(-) diff --git a/docs/sdk/mock-testing.md b/docs/sdk/mock-testing.md index 192d2786..d92fc8c9 100644 --- a/docs/sdk/mock-testing.md +++ b/docs/sdk/mock-testing.md @@ -7,96 +7,148 @@ import TabItem from '@theme/TabItem'; Mock testing allows you to test your application's behavior with specific Feature and Variable configurations without making API calls to DevCycle. This enables faster, more reliable tests that don't depend on external services. -## Methods for Mock Testing +## Mock Testing Strategy -### DevCycleUser +The general testing strategy we'd recommend is to mock DevCycle's Variable and Variable value calls as opposed to mocking the full DevCycle client. To achieve this, you'd want to perform network level mocking to intercept the Variables or Variable by key requests to provide your own Variable response. -``` -mockUser = DevCycleUser( - user_id='test_user', - email='test@example.com', - country='CA', - customData={ - customKey='value' - } -) -``` +## Server-Side SDKs: Cloud Bucketing -### Variable and VariableValue +DevCycle's Server-side SDKs can operate on Cloud or Local Bucketing modes. We'd recommend using Cloud Bucketing for mock testing. Cloud Bucketing uses the [DevCycle Bucketing API](/bucketing-api/#tag/Bucketing-API) behind the scenes to fetch and retrive Variable values. You may mock the response of the `Get Variable by Key` or `Get Variables` endpoints in order to replicate the output of your Server-side SDK. + +### Setup + +The SDK Key does not need to be a valid SDK key, but it needs to be in a correctly formatted starting with `dvc_server` or `server`. -Test retrieving a Variable value through the `.variable()` or `.variableValue()` method. ```python - def test_variable_string(self, mock_client): - """Test getting a string variable value""" - - mock_variable = Variable( - _id='var_str', - key='example-text', - type='String', - value='step-1' + def setUp(self): + self.sdk_key = "dvc_server_test_key_12345678" + self.bucketing_api_base = "https://bucketing-api.devcycle.com" + self.options = DevCycleCloudOptions( + bucketing_api_uri=self.bucketing_api_base, + request_timeout=5, + request_retries=0, ) + self.client = DevCycleCloudClient(self.sdk_key, self.options) + + def tearDown(self): + if self.client: + self.client.close() + responses.reset() + + def _get_variable_url(self, key: str) -> str: + """Get the URL for a single variable request""" + return f"{self.bucketing_api_base}/v1/variables/{key}" + + def _get_variables_url(self) -> str: + """Get the URL for all variables request""" + return f"{self.bucketing_api_base}/v1/variables" + ``` + + + +### Variable and VariableValue Test + +Test retrieving a Variable value through the `.variable()` or `.variableValue()` method. + + + + ```python + def test_variable_value_string(self): + """Test variable_value() with a string variable""" + variable_key = "string-test-var" + expected_value = "var-on" - mock_client.variable.return_value = mock_variable - - result = mock_client.variable(self.mock_user, 'example-text', 'default') - - # Assertions - self.assertEqual(result.value, 'step-1') - self.assertEqual(result.key, 'example-text') - self.assertEqual(result.type, 'String') + responses.add( + responses.POST, + self._get_variable_url(variable_key), + json={ + "_id": "600000000000000000000000", + "key": variable_key, + "type": "String", + "value": expected_value, + "eval": { + "reason": "TARGETING_MATCH", + "details": "All Users", + "target_id": "500000000000000000000000" + } + }, + status=200, + ) + + user = DevCycleUser(user_id="test-user-123") + result = self.client.variable_value(user, variable_key, "default-value") + + self.assertEqual(result, expected_value) + # Verify the request was made + self.assertEqual(len(responses.calls), 1) + self.assertEqual(responses.calls[0].request.method, "POST") ``` -### AllVariables +### AllVariables Test -Test retrieving all active Variables through the `allVariable()` or `all_variables()` method. +Test retrieving all active Variables through the `.allVariable()` method. ```python - def test_all_variables(self, mock_client): - """Test retrieving all variables for a user""" - - mock_variables = { - 'feature-flag-1': Variable( - _id='var1', - key='feature-flag-1', - type='Boolean', - value=True, - eval=EvalReason( - reason='TARGETING_MATCH', - details='ALL USERS', - target_id='targeting_rule1' - ) - ), - 'feature-flag-2': Variable( - _id='var2', - key='feature-flag-2', - type='String', - value='variant-a', - eval=EvalReason( - reason='SPLIT', - details='Random Distribution | Audience Match', - target_id='targeting_rule2' - ) - ), - } - - mock_client.all_variables.return_value = mock_variables - mock_client.is_initialized.return_value = True + def test_all_variables(self): + """Test retrieving all variables using all_variables()""" + responses.add( + responses.POST, + self._get_variables_url(), + json={ + "string-test-var": { + "_id": "636d3da931a842d858e84990", + "key": "string-test-var", + "type": "String", + "value": "var-on", + "eval": { + "reason": "TARGETING_MATCH", + "details": "All Users", + "target_id": "636d39a231a842d858e8474d" + } + }, + "bool-test-var": { + "_id": "636d39a2e013f54685049e34", + "key": "bool-test-var", + "type": "Boolean", + "value": True, + "eval": { + "reason": "TARGETING_MATCH", + "details": "All Users", + "target_id": "636d39a231a842d858e8474d" + } + } + }, + status=200, + ) + + user = DevCycleUser(user_id="test-user-123") + result = self.client.all_variables(user) + + # Verify the response structure + self.assertIsInstance(result, dict) + self.assertIn("string-test-var", result) + self.assertIn("bool-test-var", result) - # Call the method - result = mock_client.all_variables(self.mock_user) + # Verify variable values + self.assertEqual(result["string-test-var"].value, "var-on") + self.assertEqual(result["string-test-var"].key, "string-test-var") + self.assertEqual(result["string-test-var"].type, "String") - # Assertions - self.assertEqual(len(result), 2) - self.assertTrue(result['feature-flag-1'].value) - self.assertEqual(result['feature-flag-2'].value, 'variant-a') - mock_client.all_variables.assert_called_once_with(self.mock_user) + self.assertTrue(result["bool-test-var"].value) + self.assertEqual(result["bool-test-var"].key, "bool-test-var") + self.assertEqual(result["bool-test-var"].type, "Boolean") + + # Verify the request was made to the correct URL + self.assertEqual(len(responses.calls), 1) + self.assertEqual(responses.calls[0].request.method, "POST") + self.assertEqual(responses.calls[0].request.url, self._get_variables_url()) ``` @@ -117,21 +169,6 @@ const devcycleClient = initializeDevCycle( ) ``` -## Server-Side SDKs: Local Bucketing - -Server-side SDKs with Local Bucketing mode enabled, will allow you to provide the SDK a local configuration file or mock data. - -```javascript -// Example for Node.js SDK -const devcycleClient = initializeDevCycle( - '', - { - configCDN: 'https://your-test-config.com/config.json', - // or use local file - } -) -``` - ## Best Practices - Use consistent test configurations across your test suite From 0a4f66693714c1a192ee74551650ccd017673b58 Mon Sep 17 00:00:00 2001 From: Leslie Lau Date: Tue, 16 Dec 2025 14:44:39 -0500 Subject: [PATCH 3/4] Change ids to dummy ids --- docs/sdk/mock-testing.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/sdk/mock-testing.md b/docs/sdk/mock-testing.md index d92fc8c9..729732c9 100644 --- a/docs/sdk/mock-testing.md +++ b/docs/sdk/mock-testing.md @@ -65,14 +65,14 @@ Test retrieving a Variable value through the `.variable()` or `.variableValue()` responses.POST, self._get_variable_url(variable_key), json={ - "_id": "600000000000000000000000", + "_id": "12345", "key": variable_key, "type": "String", "value": expected_value, "eval": { "reason": "TARGETING_MATCH", "details": "All Users", - "target_id": "500000000000000000000000" + "target_id": "54321" } }, status=200, @@ -103,25 +103,25 @@ Test retrieving all active Variables through the `.allVariable()` method. self._get_variables_url(), json={ "string-test-var": { - "_id": "636d3da931a842d858e84990", + "_id": "123456", "key": "string-test-var", "type": "String", "value": "var-on", "eval": { "reason": "TARGETING_MATCH", "details": "All Users", - "target_id": "636d39a231a842d858e8474d" + "target_id": "654321" } }, "bool-test-var": { - "_id": "636d39a2e013f54685049e34", + "_id": "123456", "key": "bool-test-var", "type": "Boolean", "value": True, "eval": { "reason": "TARGETING_MATCH", "details": "All Users", - "target_id": "636d39a231a842d858e8474d" + "target_id": "654321" } } }, From beec6674ee1ed4b1d46d067b3b4ffe2bd2af52ea Mon Sep 17 00:00:00 2001 From: Leslie Lau Date: Tue, 6 Jan 2026 16:43:46 -0500 Subject: [PATCH 4/4] Add Client-side testing content --- docs/sdk/mock-testing.md | 164 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 153 insertions(+), 11 deletions(-) diff --git a/docs/sdk/mock-testing.md b/docs/sdk/mock-testing.md index 729732c9..a68a9244 100644 --- a/docs/sdk/mock-testing.md +++ b/docs/sdk/mock-testing.md @@ -9,22 +9,22 @@ Mock testing allows you to test your application's behavior with specific Featur ## Mock Testing Strategy -The general testing strategy we'd recommend is to mock DevCycle's Variable and Variable value calls as opposed to mocking the full DevCycle client. To achieve this, you'd want to perform network level mocking to intercept the Variables or Variable by key requests to provide your own Variable response. +The general testing strategy we'd recommend is to mock DevCycle's Variable and Variable value method calls as opposed to mocking the full DevCycle client. To achieve this, you'd want to perform network-level mocking to intercept the Variables or Variable by key requests to provide your own Variable response. This approach ensures your tests focus on application logic rather than SDK implementation details. ## Server-Side SDKs: Cloud Bucketing -DevCycle's Server-side SDKs can operate on Cloud or Local Bucketing modes. We'd recommend using Cloud Bucketing for mock testing. Cloud Bucketing uses the [DevCycle Bucketing API](/bucketing-api/#tag/Bucketing-API) behind the scenes to fetch and retrive Variable values. You may mock the response of the `Get Variable by Key` or `Get Variables` endpoints in order to replicate the output of your Server-side SDK. +DevCycle's Server-side SDKs can operate in Cloud or Local Bucketing modes. We'd recommend using Cloud Bucketing for mock testing, as it makes network-level mocking straightforward. Cloud Bucketing uses the [DevCycle Bucketing API](/bucketing-api/#tag/Bucketing-API) behind the scenes to fetch and retrieve Variable values. You may mock the response of the `Get Variable by Key` or `Get Variables` endpoints in order to replicate the output of your Server-side SDK. ### Setup -The SDK Key does not need to be a valid SDK key, but it needs to be in a correctly formatted starting with `dvc_server` or `server`. +The SDK Key does not need to be a valid SDK key, but it needs to be correctly formatted and start with `dvc_server` or `server`. ```python def setUp(self): - self.sdk_key = "dvc_server_test_key_12345678" + self.sdk_key = "dvc_server__hash" self.bucketing_api_base = "https://bucketing-api.devcycle.com" self.options = DevCycleCloudOptions( bucketing_api_uri=self.bucketing_api_base, @@ -43,15 +43,16 @@ The SDK Key does not need to be a valid SDK key, but it needs to be in a correct return f"{self.bucketing_api_base}/v1/variables/{key}" def _get_variables_url(self) -> str: - """Get the URL for all variables request""" + """Get the URL for the all variables request""" return f"{self.bucketing_api_base}/v1/variables" ``` -### Variable and VariableValue Test +### Variable and VariableValue Testing Test retrieving a Variable value through the `.variable()` or `.variableValue()` method. + @@ -89,9 +90,10 @@ Test retrieving a Variable value through the `.variable()` or `.variableValue()` -### AllVariables Test +### AllVariables Testing + +Test retrieving all active Variables through the `.allVariables()` method. -Test retrieving all active Variables through the `.allVariable()` method. @@ -153,14 +155,153 @@ Test retrieving all active Variables through the `.allVariable()` method. +## Client-Side SDKs + +DevCycle's Client-side SDKs use the DevCycle SDK API to fetch and retrieve Variable values. You can test the `variable()` and `allVariables()` methods by creating mock Variable objects to replicate the Variable response from Client-side SDK configs. + +### Setup + +Create a mock Variable object that contains the Variables and Variable values you want to test. We'd recommend creating a default Variable test case as well to ensure your application handles missing Variables gracefully. + + + + + ```javascript + describe('DevCycle Variable method mocking', () => { + + const mockVariables = { + 'togglebot-speed': { + key: 'togglebot-speed', + value: 'fast', + type: 'String', + isDefaulted: false, + eval: { + reason: "TARGETING_MATCH", + details: "All Users", + target_id: "target-1" + } + }, + 'mock-testing-variable': { + key: 'mock-testing-variable', + value: true, + type: 'Boolean', + isDefaulted: false, + eval: { + reason: "SPLIT", + details: "Random Distribution | Audience Match", + target_id: "target-2" + } + } + }; + + // Helper function to create a mock client with the shared mockVariables + const createMockDevcycleClient = () => { + return { + variable: jest.fn((key, defaultValue) => { + // Return mocked variable if it exists, otherwise return default + if (mockVariables[key]) { + return mockVariables[key]; + } + + return { + key: key, + value: defaultValue, + type: typeof defaultValue === 'string' ? 'String' : typeof defaultValue === 'boolean' ? 'Boolean' : 'Number', + isDefaulted: true, + eval: { + reason: "DEFAULT", + details: "User Not Targeted" + } + }; + }), + allVariables: jest.fn(() => { + // Return all mocked variables + return mockVariables; + }) + }; + }; + + // Test Cases... + + }); + ``` + + + +### Variable Testing + +Test retrieving a Variable value through the `.variable()` method. You may also want to test [Evaluation Reasons](/sdk/features#evaluation-reasons) if you plan on utilizing this data for reporting or analytics. + + + + + ```javascript + describe('DevCycle Variable method mocking', () => { + + // Mock DevCycleClient Setup... + + test('should return mocked variable value using variable method', () => { + const mockDevcycleClient = createMockDevcycleClient(); + + // Test variable exists in mock config + const togglebotSpeed = mockDevcycleClient.variable('togglebot-speed', 'defaultValue'); + expect(togglebotSpeed.value).toBe('fast'); + expect(togglebotSpeed.eval.reason).toBe("TARGETING_MATCH"); + + // Verify the method was called with correct arguments + expect(mockDevcycleClient.variable).toHaveBeenCalledWith('togglebot-speed', 'defaultValue'); + + // Test default value behavior + const nonExistent = mockDevcycleClient.variable('non-existent', 'default'); + expect(nonExistent.value).toBe('default'); + expect(nonExistent.eval.reason).toBe("DEFAULT"); + }); + }); + ``` + + + +### AllVariables Testing + +Test retrieving all Variables through the `.allVariables()` method. + + + + + ```javascript + describe('DevCycle Variable method mocking', () => { + + // Mock DevCycleClient Setup... + + test('should return all mocked variables using allVariables method', () => { + const mockDevcycleClient = createMockDevcycleClient(); + + // Get all variables + const allVariables = mockDevcycleClient.allVariables(); + + // Verify all variables are returned + expect(allVariables).toBeDefined(); + expect(Object.keys(allVariables)).toHaveLength(2); + expect(allVariables['togglebot-speed']).toBeDefined(); + expect(allVariables['mock-testing-variable']).toBeDefined(); + + // Verify variable values + expect(allVariables['togglebot-speed'].value).toBe('fast'); + expect(allVariables['mock-testing-variable'].value).toBe(true); + }); + }); + ``` + + + ## Client-Side SDKs: Bootstrapping -Client-side Web SDKs (JavaScript, React, Next.js) support bootstrapping, which allows you to pass a pre-configured DevCycle configuration during initialization. +Client-side Web SDKs (JavaScript, React, Next.js) support bootstrapping, which allows you to pass a pre-configured DevCycle configuration during initialization. This is particularly useful for testing scenarios where you want to avoid network requests entirely and work with a static configuration. ```javascript const bootstrapConfig = { // Your test configuration here -} +}; const devcycleClient = initializeDevCycle( '', @@ -174,4 +315,5 @@ const devcycleClient = initializeDevCycle( - Use consistent test configurations across your test suite - Test both enabled and disabled states of Features - Test Variables and Variations to ensure all code paths are covered -- Keep mock configurations close to your test files for easy maintenance \ No newline at end of file +- Keep mock configurations close to your test files for easy maintenance +- Include edge cases such as missing Variables, default values, and different evaluation reasons \ No newline at end of file