Repository: Unleash/unleash-client-python
Pull request #143 (2021-03-25 10:14:42)

Client support for custom stickiness
# Description

Adds support for custom stickiness on `FlexibleRollout` strategy and variants.

#145 

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [x] This change requires a documentation update

# How Has This Been Tested?

Please describe the tests that you ran to verify your changes.

- [x] Unit tests
- [x] Spec Tests
- [x] Integration tests / Manual Tests

# Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules

Fixes: #144, #145



Patch:
diff --git a/Makefile b/Makefile
index dae3515..f8ebd5f 100644
--- a/Makefile
+++ b/Makefile
@@ -5,7 +5,7 @@ PROJECT_NAME = UnleashClient
 #-----------------------------------------------------------------------
 # Rules of Rules : Grouped rules that _doathing_
 #-----------------------------------------------------------------------
-test: lint pytest
+test: lint pytest specification-test
 
 precommit: clean generate-requirements
 
@@ -32,6 +32,10 @@ pytest:
 	export PYTHONPATH=${ROOT_DIR}: $$PYTHONPATH && \
 	py.test --flake8 --cov ${PROJECT_NAME} tests/unit_tests
 
+specification-test:
+	export PYTHONPATH=${ROOT_DIR}: $$PYTHONPATH && \
+	py.test tests/specification_tests
+
 tox-osx:
 	tox -c tox-osx.ini --parallel auto
 
diff --git a/UnleashClient/strategies/FlexibleRolloutStrategy.py b/UnleashClient/strategies/FlexibleRolloutStrategy.py
index 0055769..a78c9fe 100644
--- a/UnleashClient/strategies/FlexibleRolloutStrategy.py
+++ b/UnleashClient/strategies/FlexibleRolloutStrategy.py
@@ -25,7 +25,7 @@ class FlexibleRollout(Strategy):
                 calculated_percentage = normalized_hash(context['sessionId'], activation_group)
             else:
                 calculated_percentage = self.random_hash()
-        elif stickiness in ['userId', 'sessionId']:
+        elif stickiness in context.keys():
             calculated_percentage = normalized_hash(context[stickiness], activation_group)
         else:
             # This also handles the stickiness == random scenario.
diff --git a/UnleashClient/variants/Variants.py b/UnleashClient/variants/Variants.py
index ccf4b05..eef7fa1 100644
--- a/UnleashClient/variants/Variants.py
+++ b/UnleashClient/variants/Variants.py
@@ -34,16 +34,19 @@ class Variants:
         return override_variant
 
     @staticmethod
-    def _get_seed(context: dict) -> str:
+    def _get_seed(context: dict, stickiness_selector: str = "default") -> str:
         """Grabs seed value from context."""
         seed = str(random.random() * 10000)
 
-        if 'userId' in context:
-            seed = context['userId']
-        elif 'sessionId' in context:
-            seed = context['sessionId']
-        elif 'remoteAddress' in context:
-            seed = context['remoteAddress']
+        if stickiness_selector == "default":
+            if 'userId' in context:
+                seed = context['userId']
+            elif 'sessionId' in context:
+                seed = context['sessionId']
+            elif 'remoteAddress' in context:
+                seed = context['remoteAddress']
+        elif stickiness_selector in context.keys():
+            seed = context[stickiness_selector]
 
         return seed
 
@@ -53,6 +56,8 @@ class Variants:
         del formatted_variation['weight']
         if 'overrides' in formatted_variation:
             del formatted_variation['overrides']
+        if 'stickiness' in formatted_variation:
+            del formatted_variation['stickiness']
         return formatted_variation
 
     def get_variant(self, context: dict) -> dict:
@@ -73,7 +78,9 @@ class Variants:
             if total_weight <= 0:
                 return fallback_variant
 
-            target = utils.normalized_hash(self._get_seed(context), self.feature_name, total_weight)
+            stickiness_selector = self.variants[0]['stickiness'] if 'stickiness' in self.variants[0].keys() else "default"
+
+            target = utils.normalized_hash(self._get_seed(context, stickiness_selector), self.feature_name, total_weight)
             counter = 0
             for variation in self.variants:
                 counter += variation['weight']
diff --git a/docs/changelog.md b/docs/changelog.md
index b1561cc..4ea4f99 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,5 +1,8 @@
 ## Next version
 
+## Next
+* (Minor) Support custom stickiness for FlexibleRollout strategy and varients.
+
 ## v4.1.0
 * (Minor) Support project-based feature flag loading.
 


Test patch:
diff --git a/tests/specification_tests/test_11_strategy_constraints_edge_cases.py b/tests/specification_tests/test_11_strategy_constraints_edge_cases.py
new file mode 100644
index 0000000..5542320
--- /dev/null
+++ b/tests/specification_tests/test_11_strategy_constraints_edge_cases.py
@@ -0,0 +1,165 @@
+import uuid
+import json
+import pytest
+import responses
+from UnleashClient import UnleashClient
+from UnleashClient.constants import FEATURES_URL, METRICS_URL, REGISTER_URL
+from tests.utilities.testing_constants import URL, APP_NAME
+
+
+MOCK_JSON = """
+{
+    "version": 1,
+    "features": [
+        {
+            "name": "Feature.constraints.no_values",
+            "description": "Not enabled with constraints and no values",
+            "enabled": true,
+            "strategies": [
+                {
+                    "name": "default",
+                    "parameters": {},
+                    "constraints": [
+                        {
+                            "contextName": "environment",
+                            "operator": "IN",
+                            "values": []
+
+                        }
+                    ]
+                }
+            ]
+        },
+        {
+            "name": "Feature.constraints.no_values_NOT_IN",
+            "description": "Is enabled with constraints and NOT_IN empty values",
+            "enabled": true,
+            "strategies": [
+                {
+                    "name": "default",
+                    "parameters": {},
+                    "constraints": [
+                        {
+                            "contextName": "environment",
+                            "operator": "NOT_IN",
+                            "values": []
+
+                        }
+                    ]
+                }
+            ]
+        },
+        {
+            "name": "Feature.constraints.empty",
+            "description": "Is enabled with empty constraints array",
+            "enabled": true,
+            "strategies": [
+                {
+                    "name": "default",
+                    "parameters": {},
+                    "constraints": []
+                }
+            ]
+        }
+    ]
+}
+"""
+
+
+@pytest.fixture()
+def unleash_client():
+    unleash_client = UnleashClient(url=URL,
+                                   app_name=APP_NAME,
+                                   instance_id='pytest_%s' % uuid.uuid4())
+    yield unleash_client
+    unleash_client.destroy()
+
+
+@responses.activate
+def test_feature_constraints_novalue_defaultenv(unleash_client):
+    """
+    Feature.constraints.no_value should not be enabled in default environment
+    """
+    # Set up API
+    responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202)
+    responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200)
+    responses.add(responses.POST, URL + METRICS_URL, json={}, status=202)
+
+    # Tests
+    unleash_client.initialize_client()
+    assert not unleash_client.is_enabled("Feature.constraints.no_values", {'environment': 'default'})
+
+
+@responses.activate
+def test_feature_constraints_novalue_emptyenv(unleash_client):
+    """
+    Feature.constraints.no_value should not be enabled in empty environment
+    """
+    # Set up API
+    responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202)
+    responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200)
+    responses.add(responses.POST, URL + METRICS_URL, json={}, status=202)
+
+    # Tests
+    unleash_client.initialize_client()
+    assert not unleash_client.is_enabled("Feature.constraints.no_values", {})
+
+
+@responses.activate
+def test_feature_constraints_novalue_defaultenv_notin(unleash_client):
+    """
+    Feature.constraints.no_values_NOT_IN should be enabled in default environment
+    """
+    # Set up API
+    responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202)
+    responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200)
+    responses.add(responses.POST, URL + METRICS_URL, json={}, status=202)
+
+    # Tests
+    unleash_client.initialize_client()
+    assert unleash_client.is_enabled("Feature.constraints.no_values_NOT_IN", {'environment': 'default'})
+
+
+@responses.activate
+def test_feature_constraints_novalue_emptyenv_notin(unleash_client):
+    """
+    Feature.constraints.no_values_NOT_IN should be enabled in empty environment
+    """
+    # Set up API
+    responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202)
+    responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200)
+    responses.add(responses.POST, URL + METRICS_URL, json={}, status=202)
+
+    # Tests
+    unleash_client.initialize_client()
+    assert unleash_client.is_enabled("Feature.constraints.no_values_NOT_IN", {})
+
+
+@responses.activate
+def test_feature_constraints_empty_defaultenv(unleash_client):
+    """
+    Feature.constraints.empty should be enabled
+    """
+    # Set up API
+    responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202)
+    responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200)
+    responses.add(responses.POST, URL + METRICS_URL, json={}, status=202)
+
+    # Tests
+    unleash_client.initialize_client()
+    assert unleash_client.is_enabled("Feature.constraints.no_values_NOT_IN", {'environment': 'default'})
+
+
+@responses.activate
+def test_feature_constraints_empty_emptyenv(unleash_client):
+    """
+    Feature.constraints.empty should be enabled
+    """
+    # Set up API
+    responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202)
+    responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200)
+    responses.add(responses.POST, URL + METRICS_URL, json={}, status=202)
+
+    # Tests
+    unleash_client.initialize_client()
+    assert unleash_client.is_enabled("Feature.constraints.no_values_NOT_IN", {})
\ No newline at end of file
diff --git a/tests/specification_tests/test_12_custom_stickiness.py b/tests/specification_tests/test_12_custom_stickiness.py
new file mode 100644
index 0000000..a3928d7
--- /dev/null
+++ b/tests/specification_tests/test_12_custom_stickiness.py
@@ -0,0 +1,315 @@
+import uuid
+import json
+import pytest
+import responses
+from UnleashClient import UnleashClient
+from UnleashClient.constants import FEATURES_URL, METRICS_URL, REGISTER_URL
+from tests.utilities.testing_constants import URL, APP_NAME
+
+
+MOCK_JSON = """
+ {
+    "version": 1,
+    "features": [
+        {
+            "name": "Feature.flexible.rollout.custom.stickiness_100",
+            "description": "Should support custom stickiness as option",
+            "enabled": true,
+            "strategies": [
+                {
+                    "name": "flexibleRollout",
+                    "parameters": {
+                        "rollout": "100",
+                        "stickiness": "customField",
+                        "groupId": "Feature.flexible.rollout.custom.stickiness_100"
+                    },
+                    "constraints": []
+                }
+            ],
+            "variants": [
+                {
+                    "name": "blue",
+                    "weight": 25,
+                    "stickiness": "customField",
+                    "payload": {
+                        "type": "string",
+                        "value": "val1"
+                    }
+                },
+                {
+                    "name": "red",
+                    "weight": 25,
+                    "stickiness": "customField",
+                    "payload": {
+                        "type": "string",
+                        "value": "val1"
+                    }
+                },
+                {
+                    "name": "green",
+                    "weight": 25,
+                    "stickiness": "customField",
+                    "payload": {
+                        "type": "string",
+                        "value": "val1"
+                    }
+                },
+                {
+                    "name": "yellow",
+                    "weight": 25,
+                    "stickiness": "customField",
+                    "payload": {
+                        "type": "string",
+                        "value": "val1"
+                    }
+                }
+            ]
+        },
+        {
+            "name": "Feature.flexible.rollout.custom.stickiness_50",
+            "description": "Should support custom stickiness as option",
+            "enabled": true,
+            "strategies": [
+                {
+                    "name": "flexibleRollout",
+                    "parameters": {
+                        "rollout": "50",
+                        "stickiness": "customField",
+                        "groupId": "Feature.flexible.rollout.custom.stickiness_50"
+                    },
+                    "constraints": []
+                }
+            ]
+        }
+    ]
+}
+"""
+
+
+@pytest.fixture()
+def unleash_client():
+    unleash_client = UnleashClient(url=URL,
+                                   app_name=APP_NAME,
+                                   instance_id='pytest_%s' % uuid.uuid4())
+    yield unleash_client
+    unleash_client.destroy()
+
+
+@responses.activate
+def test_feature_flexiblerollout_stickiness_100(unleash_client):
+    """
+    Feature.flexible.rollout.custom.stickiness_100 should be enabled without field defined for 100%
+    """
+    # Set up API
+    responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202)
+    responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200)
+    responses.add(responses.POST, URL + METRICS_URL, json={}, status=202)
+
+    # Tests
+    unleash_client.initialize_client()
+    assert unleash_client.is_enabled("Feature.flexible.rollout.custom.stickiness_100", {'customField': 'any_value'})
+
+
+@responses.activate
+def test_feature_flexiblerollout_stickiness_50_nocontext(unleash_client):
+    """
+    Feature.flexible.rollout.custom.stickiness_50 should not be enabled without custom field
+    """
+    # Set up API
+    responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202)
+    responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200)
+    responses.add(responses.POST, URL + METRICS_URL, json={}, status=202)
+
+    # Tests
+    unleash_client.initialize_client()
+    assert not unleash_client.is_enabled("Feature.flexible.rollout.custom.stickiness_50", {})
+
+
+@responses.activate
+def test_feature_flexiblerollout_stickiness_50_customfield_402(unleash_client):
+    """
+    Feature.flexible.rollout.custom.stickiness_50 should not enabled without customField=402
+    """
+    # Set up API
+    responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202)
+    responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200)
+    responses.add(responses.POST, URL + METRICS_URL, json={}, status=202)
+
+    # Tests
+    unleash_client.initialize_client()
+    assert not unleash_client.is_enabled("Feature.flexible.rollout.custom.stickiness_50", {'customField': '402'})
+
+
+@responses.activate
+def test_feature_flexiblerollout_stickiness_50_customfield_388(unleash_client):
+    """
+    Feature.flexible.rollout.custom.stickiness_50 should be enabled for customField=388
+    """
+    # Set up API
+    responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202)
+    responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200)
+    responses.add(responses.POST, URL + METRICS_URL, json={}, status=202)
+
+    # Tests
+    unleash_client.initialize_client()
+    assert unleash_client.is_enabled("Feature.flexible.rollout.custom.stickiness_50", {'customField': '388'})
+
+
+@responses.activate
+def test_feature_flexiblerollout_stickiness_50_customfield_39(unleash_client):
+    """
+    Feature.flexible.rollout.custom.stickiness_50 should be enabled without customField=39
+    """
+    # Set up API
+    responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202)
+    responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200)
+    responses.add(responses.POST, URL + METRICS_URL, json={}, status=202)
+
+    # Tests
+    unleash_client.initialize_client()
+    assert unleash_client.is_enabled("Feature.flexible.rollout.custom.stickiness_50", {'customField': '39'})
+
+
+@responses.activate
+def test_variant_flexiblerollout_stickiness_100_customfield_528(unleash_client):
+    """
+    Feature.flexible.rollout.custom.stickiness_100 and customField=528 yields blue
+    """
+    # Set up API
+    responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202)
+    responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200)
+    responses.add(responses.POST, URL + METRICS_URL, json={}, status=202)
+
+    # Tests
+    unleash_client.initialize_client()
+    context = {
+        'customField': "528"
+    }
+
+    expected_result = {
+        "name": "blue",
+        "payload": {
+            "type": "string",
+            "value": "val1"
+        },
+        "enabled": True
+    }
+
+    actual_result = unleash_client.get_variant("Feature.flexible.rollout.custom.stickiness_100", context)
+    assert actual_result == expected_result
+
+
+@responses.activate
+def test_variant_flexiblerollout_stickiness_100_customfield_16(unleash_client):
+    """
+    Feature.flexible.rollout.custom.stickiness_100 and customField=16 yields blue
+    """
+    # Set up API
+    responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202)
+    responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200)
+    responses.add(responses.POST, URL + METRICS_URL, json={}, status=202)
+
+    # Tests
+    unleash_client.initialize_client()
+    context = {
+        'customField': "16"
+    }
+
+    expected_result = {
+        "name": "blue",
+        "payload": {
+            "type": "string",
+            "value": "val1"
+        },
+        "enabled": True
+    }
+
+    actual_result = unleash_client.get_variant("Feature.flexible.rollout.custom.stickiness_100", context)
+    assert actual_result == expected_result
+
+
+@responses.activate
+def test_variant_flexiblerollout_stickiness_100_customfield_198(unleash_client):
+    """
+    Feature.flexible.rollout.custom.stickiness_100 and customField=198 yields red
+    """
+    # Set up API
+    responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202)
+    responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200)
+    responses.add(responses.POST, URL + METRICS_URL, json={}, status=202)
+
+    # Tests
+    unleash_client.initialize_client()
+    context = {
+        'customField': "198"
+    }
+
+    expected_result = {
+        "name": "red",
+        "payload": {
+            "type": "string",
+            "value": "val1"
+        },
+        "enabled": True
+    }
+
+    actual_result = unleash_client.get_variant("Feature.flexible.rollout.custom.stickiness_100", context)
+    assert actual_result == expected_result
+
+
+@responses.activate
+def test_variant_flexiblerollout_stickiness_100_customfield_43(unleash_client):
+    """
+    Feature.flexible.rollout.custom.stickiness_100 and customField=43 yields green
+    """
+    # Set up API
+    responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202)
+    responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200)
+    responses.add(responses.POST, URL + METRICS_URL, json={}, status=202)
+
+    # Tests
+    unleash_client.initialize_client()
+    context = {
+        'customField': "43"
+    }
+
+    expected_result = {
+        "name": "green",
+        "payload": {
+            "type": "string",
+            "value": "val1"
+        },
+        "enabled": True
+    }
+
+    actual_result = unleash_client.get_variant("Feature.flexible.rollout.custom.stickiness_100", context)
+    assert actual_result == expected_result
+
+
+@responses.activate
+def test_variant_flexiblerollout_stickiness_100_customfield_112(unleash_client):
+    """
+    Feature.flexible.rollout.custom.stickiness_100 and customField=112 yields yellow
+    """
+    # Set up API
+    responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202)
+    responses.add(responses.GET, URL + FEATURES_URL, json=json.loads(MOCK_JSON), status=200)
+    responses.add(responses.POST, URL + METRICS_URL, json={}, status=202)
+
+    # Tests
+    unleash_client.initialize_client()
+    context = {
+        'customField': "112"
+    }
+
+    expected_result = {
+        "name": "yellow",
+        "payload": {
+            "type": "string",
+            "value": "val1"
+        },
+        "enabled": True
+    }
+
+    actual_result = unleash_client.get_variant("Feature.flexible.rollout.custom.stickiness_100", context)
+    assert actual_result == expected_result
diff --git a/tests/unit_tests/strategies/test_flexiblerollout.py b/tests/unit_tests/strategies/test_flexiblerollout.py
index 434e56f..834ebdc 100644
--- a/tests/unit_tests/strategies/test_flexiblerollout.py
+++ b/tests/unit_tests/strategies/test_flexiblerollout.py
@@ -92,6 +92,15 @@ def test_flexiblerollout_random(strategy):
     assert strategy.execute(base_context) in [True, False]
 
 
+def test_flexiblerollout_customfield(strategy):
+    BASE_FLEXIBLE_ROLLOUT_DICT['parameters']['stickiness'] = 'customField'
+    base_context = dict(appName='test', environment='prod', userId="9")
+    base_context['customField'] = "122"
+    assert strategy.execute(base_context)
+    base_context['customField'] = "155"
+    assert not strategy.execute(base_context)
+
+
 def test_flexiblerollout_default():
     BASE_FLEXIBLE_ROLLOUT_DICT['parameters']['stickiness'] = 'default'
     BASE_FLEXIBLE_ROLLOUT_DICT['constraints'] = [x for x in BASE_FLEXIBLE_ROLLOUT_DICT['constraints'] if x['contextName'] != 'userId']
diff --git a/tests/unit_tests/test_variants.py b/tests/unit_tests/test_variants.py
index c23c18e..a0a5a32 100644
--- a/tests/unit_tests/test_variants.py
+++ b/tests/unit_tests/test_variants.py
@@ -1,6 +1,6 @@
 import pytest
 from UnleashClient.variants import Variants
-from tests.utilities.mocks.mock_variants import VARIANTS
+from tests.utilities.mocks.mock_variants import VARIANTS, VARIANTS_WITH_STICKINESS
 
 
 @pytest.fixture()
@@ -8,6 +8,11 @@ def variations():
     yield Variants(VARIANTS, "TestFeature")
 
 
+@pytest.fixture()
+def variations_with_stickiness():
+    yield Variants(VARIANTS_WITH_STICKINESS, "TestFeature")
+
+
 def test_variations_override_match(variations):
     override_variant = variations._apply_overrides({'userId': '1'})
     assert override_variant['name'] == 'VarA'
@@ -37,6 +42,18 @@ def test_variations_seed(variations):
     assert context['remoteAddress'] == variations._get_seed(context)
 
 
+def test_variations_seed_override(variations):
+    # UserId, SessionId, and remoteAddress
+    context = {
+        'userId': '1',
+        'sessionId': '1',
+        'remoteAddress': '1.1.1.1',
+        'customField': "ActuallyAmAHamster"
+    }
+
+    assert context['customField'] == variations._get_seed(context, 'customField')
+
+
 def test_variation_selectvariation_happypath(variations):
     variant = variations.get_variant({'userId': '2'})
     assert variant
@@ -44,6 +61,14 @@ def test_variation_selectvariation_happypath(variations):
     assert variant['name'] == 'VarC'
 
 
+def test_variation_customvariation(variations_with_stickiness):
+    variations = variations_with_stickiness
+    variant = variations.get_variant({'customField': 'ActuallyAmAHamster1234'})
+    assert variant
+    assert 'payload' in variant
+    assert variant['name'] == 'VarC'
+
+
 def test_variation_selectvariation_multi(variations):
     tracker = {}
     for x in range(100):
diff --git a/tests/utilities/mocks/mock_variants.py b/tests/utilities/mocks/mock_variants.py
index 990ff1a..2c0ab2c 100644
--- a/tests/utilities/mocks/mock_variants.py
+++ b/tests/utilities/mocks/mock_variants.py
@@ -33,3 +33,42 @@ VARIANTS = \
             }
         }
     ]
+
+VARIANTS_WITH_STICKINESS = \
+    [
+        {
+            "name": "VarA",
+            "weight": 34,
+            "stickiness": "customField",
+            "payload": {
+                "type": "string",
+                "value": "Test1"
+            },
+            "overrides": [
+                {
+                    "contextName": "userId",
+                    "values": [
+                        "1"
+                    ]
+                }
+            ]
+        },
+        {
+            "name": "VarB",
+            "weight": 33,
+            "stickiness": "customField",
+            "payload": {
+                "type": "string",
+                "value": "Test 2"
+            }
+        },
+        {
+            "name": "VarC",
+            "weight": 33,
+"stickiness": "customField",
+            "payload": {
+                "type": "string",
+                "value": "Test 3"
+            }
+        }
+    ]