diff --git a/optimizely/optimizely.py b/optimizely/optimizely.py index 7bd125e3..63aefa77 100644 --- a/optimizely/optimizely.py +++ b/optimizely/optimizely.py @@ -203,8 +203,10 @@ def _get_feature_variable_for_type(self, feature_key, variable_key, variable_typ if not variable: return None - # Return None if type differs - if variable.type != variable_type: + # For non-typed method, use type of variable; else, return None if type differs + if not variable_type: + variable_type = variable.type + elif variable.type != variable_type: self.logger.warning( 'Requested variable type "%s", but variable is of type "%s". ' 'Use correct API to retrieve value. Returning None.' % (variable_type, variable.type) @@ -513,6 +515,23 @@ def get_enabled_features(self, user_id, attributes=None): return enabled_features + def get_feature_variable(self, feature_key, variable_key, user_id, attributes=None): + """ Returns value for a variable attached to a feature flag. + + Args: + feature_key: Key of the feature whose variable's value is being accessed. + variable_key: Key of the variable whose value is to be accessed. + user_id: ID for user. + attributes: Dict representing user attributes. + + Returns: + Value of the variable. None if: + - Feature key is invalid. + - Variable key is invalid. + """ + + return self._get_feature_variable_for_type(feature_key, variable_key, None, user_id, attributes) + def get_feature_variable_boolean(self, feature_key, variable_key, user_id, attributes=None): """ Returns value for a certain boolean variable attached to a feature flag. diff --git a/tests/test_optimizely.py b/tests/test_optimizely.py index cf9dc8ef..b9c9c8a2 100644 --- a/tests/test_optimizely.py +++ b/tests/test_optimizely.py @@ -2148,6 +2148,141 @@ def test_get_feature_variable_string(self): } ) + def test_get_feature_variable(self): + """ Test that get_feature_variable returns variable value as expected \ + and broadcasts decision with proper parameters. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + mock_experiment = opt_obj.config.get_experiment_from_key('test_experiment') + mock_variation = opt_obj.config.get_variation_from_id('test_experiment', '111129') + # Boolean + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, + mock_variation, + enums.DecisionSources.FEATURE_TEST)), \ + mock.patch.object(opt_obj.config, 'logger') as mock_config_logging, \ + mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: + self.assertTrue(opt_obj.get_feature_variable('test_feature_in_experiment', 'is_working', 'test_user')) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "is_working" for variation "variation" is "true".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': True, + 'source': 'feature-test', + 'variable_key': 'is_working', + 'variable_value': True, + 'variable_type': 'boolean', + 'source_info': { + 'experiment_key': 'test_experiment', + 'variation_key': 'variation' + } + } + ) + # Double + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, + mock_variation, + enums.DecisionSources.FEATURE_TEST)), \ + mock.patch.object(opt_obj.config, 'logger') as mock_config_logging, \ + mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: + self.assertEqual(10.02, opt_obj.get_feature_variable('test_feature_in_experiment', 'cost', 'test_user')) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "cost" for variation "variation" is "10.02".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': True, + 'source': 'feature-test', + 'variable_key': 'cost', + 'variable_value': 10.02, + 'variable_type': 'double', + 'source_info': { + 'experiment_key': 'test_experiment', + 'variation_key': 'variation' + } + } + ) + # Integer + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, + mock_variation, + enums.DecisionSources.FEATURE_TEST)), \ + mock.patch.object(opt_obj.config, 'logger') as mock_config_logging, \ + mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: + self.assertEqual(4243, opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user')) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "count" for variation "variation" is "4243".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': True, + 'source': 'feature-test', + 'variable_key': 'count', + 'variable_value': 4243, + 'variable_type': 'integer', + 'source_info': { + 'experiment_key': 'test_experiment', + 'variation_key': 'variation' + } + } + ) + # String + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, + mock_variation, + enums.DecisionSources.FEATURE_TEST)), \ + mock.patch.object(opt_obj.config, 'logger') as mock_config_logging, \ + mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: + self.assertEqual( + 'staging', + opt_obj.get_feature_variable('test_feature_in_experiment', 'environment', 'test_user') + ) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "environment" for variation "variation" is "staging".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': True, + 'source': 'feature-test', + 'variable_key': 'environment', + 'variable_value': 'staging', + 'variable_type': 'string', + 'source_info': { + 'experiment_key': 'test_experiment', + 'variation_key': 'variation' + } + } + ) + def test_get_feature_variable_boolean_for_feature_in_rollout(self): """ Test that get_feature_variable_boolean returns Boolean value as expected \ and broadcasts decision with proper parameters. """ @@ -2225,7 +2360,7 @@ def test_get_feature_variable_double_for_feature_in_rollout(self): ) def test_get_feature_variable_integer_for_feature_in_rollout(self): - """ Test that get_feature_variable_double returns Double value as expected \ + """ Test that get_feature_variable_integer returns Double value as expected \ and broadcasts decision with proper parameters. """ opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) @@ -2300,6 +2435,132 @@ def test_get_feature_variable_string_for_feature_in_rollout(self): } ) + def test_get_feature_variable_for_feature_in_rollout(self): + """ Test that get_feature_variable returns value as expected \ + and broadcasts decision with proper parameters. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + mock_experiment = opt_obj.config.get_experiment_from_key('211127') + mock_variation = opt_obj.config.get_variation_from_id('211127', '211129') + user_attributes = {'test_attribute': 'test_value'} + + # Boolean + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, + mock_variation, + enums.DecisionSources.ROLLOUT)), \ + mock.patch.object(opt_obj.config, 'logger') as mock_config_logging, \ + mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: + self.assertTrue(opt_obj.get_feature_variable('test_feature_in_rollout', 'is_running', 'test_user', + attributes=user_attributes)) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "is_running" for variation "211129" is "true".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {'test_attribute': 'test_value'}, + { + 'feature_key': 'test_feature_in_rollout', + 'feature_enabled': True, + 'source': 'rollout', + 'variable_key': 'is_running', + 'variable_value': True, + 'variable_type': 'boolean', + 'source_info': {} + } + ) + # Double + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, + mock_variation, + enums.DecisionSources.ROLLOUT)), \ + mock.patch.object(opt_obj.config, 'logger') as mock_config_logging, \ + mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: + self.assertTrue(opt_obj.get_feature_variable('test_feature_in_rollout', 'price', 'test_user', + attributes=user_attributes)) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "price" for variation "211129" is "39.99".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {'test_attribute': 'test_value'}, + { + 'feature_key': 'test_feature_in_rollout', + 'feature_enabled': True, + 'source': 'rollout', + 'variable_key': 'price', + 'variable_value': 39.99, + 'variable_type': 'double', + 'source_info': {} + } + ) + # Integer + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, + mock_variation, + enums.DecisionSources.ROLLOUT)), \ + mock.patch.object(opt_obj.config, 'logger') as mock_config_logging, \ + mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: + self.assertTrue(opt_obj.get_feature_variable('test_feature_in_rollout', 'count', 'test_user', + attributes=user_attributes)) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "count" for variation "211129" is "399".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {'test_attribute': 'test_value'}, + { + 'feature_key': 'test_feature_in_rollout', + 'feature_enabled': True, + 'source': 'rollout', + 'variable_key': 'count', + 'variable_value': 399, + 'variable_type': 'integer', + 'source_info': {} + } + ) + # String + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, + mock_variation, + enums.DecisionSources.ROLLOUT)), \ + mock.patch.object(opt_obj.config, 'logger') as mock_config_logging, \ + mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: + self.assertTrue(opt_obj.get_feature_variable('test_feature_in_rollout', 'message', 'test_user', + attributes=user_attributes)) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "message" for variation "211129" is "Hello audience".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {'test_attribute': 'test_value'}, + { + 'feature_key': 'test_feature_in_rollout', + 'feature_enabled': True, + 'source': 'rollout', + 'variable_key': 'message', + 'variable_value': 'Hello audience', + 'variable_type': 'string', + 'source_info': {} + } + ) + def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_variation(self): """ Test that get_feature_variable_* returns default value if variable usage not present in variation. """ @@ -2330,50 +2591,225 @@ def test_get_feature_variable__returns_default_value_if_variable_usage_not_in_va self.assertEqual(10.99, opt_obj.get_feature_variable_double('test_feature_in_experiment', 'cost', 'test_user')) - mock_config_logger.info.assert_called_once_with( - 'Variable "cost" is not used in variation "variation". Assigning default value "10.99".' + mock_config_logger.info.assert_called_once_with( + 'Variable "cost" is not used in variation "variation". Assigning default value "10.99".' + ) + mock_config_logger.info.reset_mock() + + # Integer + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST)), \ + mock.patch.object(opt_obj.config, 'logger') as mock_config_logger: + self.assertEqual(999, + opt_obj.get_feature_variable_integer('test_feature_in_experiment', 'count', 'test_user')) + + mock_config_logger.info.assert_called_once_with( + 'Variable "count" is not used in variation "variation". Assigning default value "999".' + ) + mock_config_logger.info.reset_mock() + + # String + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST)), \ + mock.patch.object(opt_obj.config, 'logger') as mock_config_logger: + self.assertEqual('devel', + opt_obj.get_feature_variable_string('test_feature_in_experiment', 'environment', 'test_user')) + + mock_config_logger.info.assert_called_once_with( + 'Variable "environment" is not used in variation "variation". Assigning default value "devel".' + ) + mock_config_logger.info.reset_mock() + + # Non-typed + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST)), \ + mock.patch.object(opt_obj.config, 'logger') as mock_config_logger: + self.assertTrue(opt_obj.get_feature_variable('test_feature_in_experiment', 'is_working', 'test_user')) + + mock_config_logger.info.assert_called_once_with( + 'Variable "is_working" is not used in variation "variation". Assigning default value "true".' + ) + mock_config_logger.info.reset_mock() + + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST)), \ + mock.patch.object(opt_obj.config, 'logger') as mock_config_logger: + self.assertEqual(10.99, + opt_obj.get_feature_variable('test_feature_in_experiment', 'cost', 'test_user')) + + mock_config_logger.info.assert_called_once_with( + 'Variable "cost" is not used in variation "variation". Assigning default value "10.99".' + ) + mock_config_logger.info.reset_mock() + + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST)), \ + mock.patch.object(opt_obj.config, 'logger') as mock_config_logger: + self.assertEqual(999, + opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user')) + + mock_config_logger.info.assert_called_once_with( + 'Variable "count" is not used in variation "variation". Assigning default value "999".' + ) + mock_config_logger.info.reset_mock() + + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST)), \ + mock.patch.object(opt_obj.config, 'logger') as mock_config_logger: + self.assertEqual('devel', + opt_obj.get_feature_variable('test_feature_in_experiment', 'environment', 'test_user')) + + mock_config_logger.info.assert_called_once_with( + 'Variable "environment" is not used in variation "variation". Assigning default value "devel".' + ) + mock_config_logger.info.reset_mock() + + def test_get_feature_variable__returns_default_value_if_no_variation(self): + """ Test that get_feature_variable_* returns default value if no variation \ + and broadcasts decision with proper parameters. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + # Boolean + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(None, None, + enums.DecisionSources.ROLLOUT)), \ + mock.patch.object(opt_obj, 'logger') as mock_client_logger, \ + mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: + self.assertTrue(opt_obj.get_feature_variable_boolean('test_feature_in_experiment', 'is_working', 'test_user')) + + mock_client_logger.info.assert_called_once_with( + 'User "test_user" is not in any variation or rollout rule. ' + 'Returning default value for variable "is_working" of feature flag "test_feature_in_experiment".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': False, + 'source': 'rollout', + 'variable_key': 'is_working', + 'variable_value': True, + 'variable_type': 'boolean', + 'source_info': {} + } + ) + + mock_client_logger.info.reset_mock() + + # Double + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(None, None, + enums.DecisionSources.ROLLOUT)), \ + mock.patch.object(opt_obj, 'logger') as mock_client_logger, \ + mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: + self.assertEqual(10.99, + opt_obj.get_feature_variable_double('test_feature_in_experiment', 'cost', 'test_user')) + + mock_client_logger.info.assert_called_once_with( + 'User "test_user" is not in any variation or rollout rule. ' + 'Returning default value for variable "cost" of feature flag "test_feature_in_experiment".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': False, + 'source': 'rollout', + 'variable_key': 'cost', + 'variable_value': 10.99, + 'variable_type': 'double', + 'source_info': {} + } ) - mock_config_logger.info.reset_mock() + + mock_client_logger.info.reset_mock() # Integer with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, - enums.DecisionSources.FEATURE_TEST)), \ - mock.patch.object(opt_obj.config, 'logger') as mock_config_logger: + return_value=decision_service.Decision(None, None, + enums.DecisionSources.ROLLOUT)), \ + mock.patch.object(opt_obj, 'logger') as mock_client_logger, \ + mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: self.assertEqual(999, opt_obj.get_feature_variable_integer('test_feature_in_experiment', 'count', 'test_user')) - mock_config_logger.info.assert_called_once_with( - 'Variable "count" is not used in variation "variation". Assigning default value "999".' + mock_client_logger.info.assert_called_once_with( + 'User "test_user" is not in any variation or rollout rule. ' + 'Returning default value for variable "count" of feature flag "test_feature_in_experiment".' ) - mock_config_logger.info.reset_mock() + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': False, + 'source': 'rollout', + 'variable_key': 'count', + 'variable_value': 999, + 'variable_type': 'integer', + 'source_info': {} + } + ) + + mock_client_logger.info.reset_mock() # String with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', - return_value=decision_service.Decision(mock_experiment, mock_variation, - enums.DecisionSources.FEATURE_TEST)), \ - mock.patch.object(opt_obj.config, 'logger') as mock_config_logger: + return_value=decision_service.Decision(None, None, + enums.DecisionSources.ROLLOUT)), \ + mock.patch.object(opt_obj, 'logger') as mock_client_logger, \ + mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: self.assertEqual('devel', opt_obj.get_feature_variable_string('test_feature_in_experiment', 'environment', 'test_user')) - mock_config_logger.info.assert_called_once_with( - 'Variable "environment" is not used in variation "variation". Assigning default value "devel".' + mock_client_logger.info.assert_called_once_with( + 'User "test_user" is not in any variation or rollout rule. ' + 'Returning default value for variable "environment" of feature flag "test_feature_in_experiment".' ) - mock_config_logger.info.reset_mock() - def test_get_feature_variable__returns_default_value_if_no_variation(self): - """ Test that get_feature_variable_* returns default value if no variation \ - and broadcasts decision with proper parameters. """ + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': False, + 'source': 'rollout', + 'variable_key': 'environment', + 'variable_value': 'devel', + 'variable_type': 'string', + 'source_info': {} + } + ) - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + mock_client_logger.info.reset_mock() - # Boolean + # Non-typed with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT)), \ mock.patch.object(opt_obj, 'logger') as mock_client_logger, \ mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertTrue(opt_obj.get_feature_variable_boolean('test_feature_in_experiment', 'is_working', 'test_user')) + self.assertTrue(opt_obj.get_feature_variable('test_feature_in_experiment', 'is_working', 'test_user')) mock_client_logger.info.assert_called_once_with( 'User "test_user" is not in any variation or rollout rule. ' @@ -2398,14 +2834,13 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): mock_client_logger.info.reset_mock() - # Double with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT)), \ mock.patch.object(opt_obj, 'logger') as mock_client_logger, \ mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: self.assertEqual(10.99, - opt_obj.get_feature_variable_double('test_feature_in_experiment', 'cost', 'test_user')) + opt_obj.get_feature_variable('test_feature_in_experiment', 'cost', 'test_user')) mock_client_logger.info.assert_called_once_with( 'User "test_user" is not in any variation or rollout rule. ' @@ -2430,14 +2865,13 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): mock_client_logger.info.reset_mock() - # Integer with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT)), \ mock.patch.object(opt_obj, 'logger') as mock_client_logger, \ mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: self.assertEqual(999, - opt_obj.get_feature_variable_integer('test_feature_in_experiment', 'count', 'test_user')) + opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user')) mock_client_logger.info.assert_called_once_with( 'User "test_user" is not in any variation or rollout rule. ' @@ -2462,14 +2896,13 @@ def test_get_feature_variable__returns_default_value_if_no_variation(self): mock_client_logger.info.reset_mock() - # String with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', return_value=decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT)), \ mock.patch.object(opt_obj, 'logger') as mock_client_logger, \ mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: self.assertEqual('devel', - opt_obj.get_feature_variable_string('test_feature_in_experiment', 'environment', 'test_user')) + opt_obj.get_feature_variable('test_feature_in_experiment', 'environment', 'test_user')) mock_client_logger.info.assert_called_once_with( 'User "test_user" is not in any variation or rollout rule. ' @@ -2517,6 +2950,11 @@ def test_get_feature_variable__returns_none_if_none_feature_key(self): mock_client_logger.error.assert_called_with('Provided "feature_key" is in an invalid format.') mock_client_logger.reset_mock() + # Check for non-typed + self.assertIsNone(opt_obj.get_feature_variable(None, 'variable_key', 'test_user')) + mock_client_logger.error.assert_called_with('Provided "feature_key" is in an invalid format.') + mock_client_logger.reset_mock() + def test_get_feature_variable__returns_none_if_none_variable_key(self): """ Test that get_feature_variable_* returns None for None variable key. """ @@ -2542,6 +2980,11 @@ def test_get_feature_variable__returns_none_if_none_variable_key(self): mock_client_logger.error.assert_called_with('Provided "variable_key" is in an invalid format.') mock_client_logger.reset_mock() + # Check for non-typed + self.assertIsNone(opt_obj.get_feature_variable('feature_key', None, 'test-User')) + mock_client_logger.error.assert_called_with('Provided "variable_key" is in an invalid format.') + mock_client_logger.reset_mock() + def test_get_feature_variable__returns_none_if_none_user_id(self): """ Test that get_feature_variable_* returns None for None user ID. """ @@ -2567,6 +3010,11 @@ def test_get_feature_variable__returns_none_if_none_user_id(self): mock_client_logger.error.assert_called_with('Provided "user_id" is in an invalid format.') mock_client_logger.reset_mock() + # Check for non-typed + self.assertIsNone(opt_obj.get_feature_variable('feature_key', 'variable_key', None)) + mock_client_logger.error.assert_called_with('Provided "user_id" is in an invalid format.') + mock_client_logger.reset_mock() + def test_get_feature_variable__invalid_attributes(self): """ Test that get_feature_variable_* returns None for invalid attributes. """ @@ -2613,6 +3061,41 @@ def test_get_feature_variable__invalid_attributes(self): mock_validator.reset_mock() mock_client_logging.reset_mock() + # get_feature_variable + self.assertIsNone( + opt_obj.get_feature_variable('test_feature_in_experiment', + 'is_working', 'test_user', attributes='invalid') + ) + mock_validator.assert_called_once_with('invalid') + mock_client_logging.error.assert_called_once_with('Provided attributes are in an invalid format.') + mock_validator.reset_mock() + mock_client_logging.reset_mock() + + self.assertIsNone( + opt_obj.get_feature_variable('test_feature_in_experiment', 'cost', 'test_user', attributes='invalid') + ) + mock_validator.assert_called_once_with('invalid') + mock_client_logging.error.assert_called_once_with('Provided attributes are in an invalid format.') + mock_validator.reset_mock() + mock_client_logging.reset_mock() + + self.assertIsNone( + opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user', attributes='invalid') + ) + mock_validator.assert_called_once_with('invalid') + mock_client_logging.error.assert_called_once_with('Provided attributes are in an invalid format.') + mock_validator.reset_mock() + mock_client_logging.reset_mock() + + self.assertIsNone( + opt_obj.get_feature_variable('test_feature_in_experiment', + 'environment', 'test_user', attributes='invalid') + ) + mock_validator.assert_called_once_with('invalid') + mock_client_logging.error.assert_called_once_with('Provided attributes are in an invalid format.') + mock_validator.reset_mock() + mock_client_logging.reset_mock() + def test_get_feature_variable__returns_none_if_invalid_feature_key(self): """ Test that get_feature_variable_* returns None for invalid feature key. """ @@ -2622,9 +3105,17 @@ def test_get_feature_variable__returns_none_if_invalid_feature_key(self): self.assertIsNone(opt_obj.get_feature_variable_double('invalid_feature', 'cost', 'test_user')) self.assertIsNone(opt_obj.get_feature_variable_integer('invalid_feature', 'count', 'test_user')) self.assertIsNone(opt_obj.get_feature_variable_string('invalid_feature', 'environment', 'test_user')) + self.assertIsNone(opt_obj.get_feature_variable('invalid_feature', 'is_working', 'test_user')) + self.assertIsNone(opt_obj.get_feature_variable('invalid_feature', 'cost', 'test_user')) + self.assertIsNone(opt_obj.get_feature_variable('invalid_feature', 'count', 'test_user')) + self.assertIsNone(opt_obj.get_feature_variable('invalid_feature', 'environment', 'test_user')) - self.assertEqual(4, mock_config_logger.error.call_count) + self.assertEqual(8, mock_config_logger.error.call_count) mock_config_logger.error.assert_has_calls([ + mock.call('Feature "invalid_feature" is not in datafile.'), + mock.call('Feature "invalid_feature" is not in datafile.'), + mock.call('Feature "invalid_feature" is not in datafile.'), + mock.call('Feature "invalid_feature" is not in datafile.'), mock.call('Feature "invalid_feature" is not in datafile.'), mock.call('Feature "invalid_feature" is not in datafile.'), mock.call('Feature "invalid_feature" is not in datafile.'), @@ -2648,11 +3139,16 @@ def test_get_feature_variable__returns_none_if_invalid_variable_key(self): self.assertIsNone(opt_obj.get_feature_variable_string('test_feature_in_experiment', 'invalid_variable', 'test_user')) - self.assertEqual(4, mock_config_logger.error.call_count) + self.assertIsNone(opt_obj.get_feature_variable('test_feature_in_experiment', + 'invalid_variable', + 'test_user')) + + self.assertEqual(5, mock_config_logger.error.call_count) mock_config_logger.error.assert_has_calls([ mock.call('Variable with key "invalid_variable" not found in the datafile.'), mock.call('Variable with key "invalid_variable" not found in the datafile.'), mock.call('Variable with key "invalid_variable" not found in the datafile.'), + mock.call('Variable with key "invalid_variable" not found in the datafile.'), mock.call('Variable with key "invalid_variable" not found in the datafile.') ]) @@ -2715,6 +3211,55 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled(self 'Returning the default variable value "devel".' ) + # Non-typed + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST)), \ + mock.patch.object(opt_obj, 'logger') as mock_client_logger: + + self.assertTrue(opt_obj.get_feature_variable('test_feature_in_experiment', 'is_working', 'test_user')) + + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_experiment" for variation "control" is not enabled. ' + 'Returning the default variable value "true".' + ) + + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST)), \ + mock.patch.object(opt_obj, 'logger') as mock_client_logger: + self.assertEqual(10.99, + opt_obj.get_feature_variable('test_feature_in_experiment', 'cost', 'test_user')) + + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_experiment" for variation "control" is not enabled. ' + 'Returning the default variable value "10.99".' + ) + + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST)), \ + mock.patch.object(opt_obj, 'logger') as mock_client_logger: + self.assertEqual(999, + opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user')) + + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_experiment" for variation "control" is not enabled. ' + 'Returning the default variable value "999".' + ) + + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.FEATURE_TEST)), \ + mock.patch.object(opt_obj, 'logger') as mock_client_logger: + self.assertEqual('devel', + opt_obj.get_feature_variable('test_feature_in_experiment', 'environment', 'test_user')) + + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_experiment" for variation "control" is not enabled. ' + 'Returning the default variable value "devel".' + ) + def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_rollout(self): """ Test that get_feature_variable_* returns default value if feature is not enabled for the user. """ @@ -2772,6 +3317,53 @@ def test_get_feature_variable__returns_default_value_if_feature_not_enabled_in_r 'Returning the default variable value "Hello".' ) + # Non-typed + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.ROLLOUT)), \ + mock.patch.object(opt_obj, 'logger') as mock_client_logger: + self.assertFalse(opt_obj.get_feature_variable('test_feature_in_rollout', 'is_running', 'test_user')) + + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_rollout" for variation "211229" is not enabled. ' + 'Returning the default variable value "false".' + ) + + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.ROLLOUT)), \ + mock.patch.object(opt_obj, 'logger') as mock_client_logger: + self.assertEqual(99.99, + opt_obj.get_feature_variable('test_feature_in_rollout', 'price', 'test_user')) + + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_rollout" for variation "211229" is not enabled. ' + 'Returning the default variable value "99.99".' + ) + + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.ROLLOUT)), \ + mock.patch.object(opt_obj, 'logger') as mock_client_logger: + self.assertEqual(999, + opt_obj.get_feature_variable('test_feature_in_rollout', 'count', 'test_user')) + + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_rollout" for variation "211229" is not enabled. ' + 'Returning the default variable value "999".' + ) + + with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', + return_value=decision_service.Decision(mock_experiment, mock_variation, + enums.DecisionSources.ROLLOUT)), \ + mock.patch.object(opt_obj, 'logger') as mock_client_logger: + self.assertEqual('Hello', + opt_obj.get_feature_variable('test_feature_in_rollout', 'message', 'test_user')) + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_rollout" for variation "211229" is not enabled. ' + 'Returning the default variable value "Hello".' + ) + def test_get_feature_variable__returns_none_if_type_mismatch(self): """ Test that get_feature_variable_* returns None if type mismatch. """ @@ -2805,6 +3397,7 @@ def test_get_feature_variable__returns_none_if_unable_to_cast(self): side_effect=ValueError()),\ mock.patch.object(opt_obj, 'logger') as mock_client_logger: self.assertEqual(None, opt_obj.get_feature_variable_integer('test_feature_in_experiment', 'count', 'test_user')) + self.assertEqual(None, opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user')) mock_client_logger.error.assert_called_with('Unable to cast value. Returning None.') @@ -2819,10 +3412,18 @@ def test_get_feature_variable_returns__variable_value__typed_audience_match(self 'xyz', opt_obj.get_feature_variable_string('feat_with_var', 'x', 'user1', {'lasers': 71}) ) + mock_client_logger.info.assert_called_once_with( + 'Got variable value "xyz" for variable "x" of feature flag "feat_with_var".' + ) - mock_client_logger.info.assert_called_once_with( - 'Got variable value "xyz" for variable "x" of feature flag "feat_with_var".' - ) + with mock.patch.object(opt_obj, 'logger') as mock_client_logger: + self.assertEqual( + 'xyz', + opt_obj.get_feature_variable('feat_with_var', 'x', 'user1', {'lasers': 71}) + ) + mock_client_logger.info.assert_called_once_with( + 'Got variable value "xyz" for variable "x" of feature flag "feat_with_var".' + ) # Should be included in the feature test via exact match boolean audience with id '3468206643' with mock.patch.object(opt_obj, 'logger') as mock_client_logger: @@ -2830,13 +3431,21 @@ def test_get_feature_variable_returns__variable_value__typed_audience_match(self 'xyz', opt_obj.get_feature_variable_string('feat_with_var', 'x', 'user1', {'should_do_it': True}) ) + mock_client_logger.info.assert_called_once_with( + 'Got variable value "xyz" for variable "x" of feature flag "feat_with_var".' + ) - mock_client_logger.info.assert_called_once_with( - 'Got variable value "xyz" for variable "x" of feature flag "feat_with_var".' - ) + with mock.patch.object(opt_obj, 'logger') as mock_client_logger: + self.assertEqual( + 'xyz', + opt_obj.get_feature_variable('feat_with_var', 'x', 'user1', {'should_do_it': True}) + ) + mock_client_logger.info.assert_called_once_with( + 'Got variable value "xyz" for variable "x" of feature flag "feat_with_var".' + ) - def test_get_feature_variable_returns__default_value__typed_audience_match(self): """ Test that get_feature_variable_* return default value with typed audience mismatch. """ + def test_get_feature_variable_returns__default_value__typed_audience_match(self): opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) @@ -2844,6 +3453,10 @@ def test_get_feature_variable_returns__default_value__typed_audience_match(self) 'x', opt_obj.get_feature_variable_string('feat_with_var', 'x', 'user1', {'lasers': 50}) ) + self.assertEqual( + 'x', + opt_obj.get_feature_variable('feat_with_var', 'x', 'user1', {'lasers': 50}) + ) def test_get_feature_variable_returns__variable_value__complex_audience_match(self): """ Test that get_feature_variable_* return variable value with complex audience match. """ @@ -2857,6 +3470,10 @@ def test_get_feature_variable_returns__variable_value__complex_audience_match(se 150, opt_obj.get_feature_variable_integer('feat2_with_var', 'z', 'user1', user_attr) ) + self.assertEqual( + 150, + opt_obj.get_feature_variable('feat2_with_var', 'z', 'user1', user_attr) + ) def test_get_feature_variable_returns__default_value__complex_audience_match(self): """ Test that get_feature_variable_* return default value with complex audience mismatch. """ @@ -2868,6 +3485,10 @@ def test_get_feature_variable_returns__default_value__complex_audience_match(sel 10, opt_obj.get_feature_variable_integer('feat2_with_var', 'z', 'user1', {}) ) + self.assertEqual( + 10, + opt_obj.get_feature_variable('feat2_with_var', 'z', 'user1', {}) + ) class OptimizelyWithExceptionTest(base.BaseTest):