From 2af4a370c5db962ef2066f292790d6b0c5269f7e Mon Sep 17 00:00:00 2001 From: Daniel Pons Date: Tue, 3 Aug 2021 11:20:34 +0200 Subject: [PATCH 1/2] fix(tests): Run tests on English culture Set the current thread culture to english, as for systems on others cultures, some tests were failing. --- .../OptimizelySDK.Tests.csproj | 1 + OptimizelySDK.Tests/TestSetup.cs | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 OptimizelySDK.Tests/TestSetup.cs diff --git a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj index 1a4e7df1..d5231738 100644 --- a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj +++ b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj @@ -97,6 +97,7 @@ + diff --git a/OptimizelySDK.Tests/TestSetup.cs b/OptimizelySDK.Tests/TestSetup.cs new file mode 100644 index 00000000..5f77494c --- /dev/null +++ b/OptimizelySDK.Tests/TestSetup.cs @@ -0,0 +1,25 @@ +using NUnit.Framework; +using System.Globalization; +using System.Threading; + +namespace OptimizelySDK.Tests +{ + [SetUpFixture] + public class TestSetup + { + [SetUp] + public void Init() + { + /* There are some issues doing assertions on tests with floating point numbers using the .ToString() + * method, as it's culture dependent. EG: TestGetFeatureVariableValueForTypeGivenFeatureFlagIsNotEnabledForUser, + * assigning the culture to English will make this kind of tests to work on others culture based systems. */ + Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US"); + } + + [TearDown] + public void Cleanup() + { + // Empty, but required: https://nunit.org/nunitv2/docs/2.6.4/setupFixture.html + } + } +} From 2b06f71dd78d85b15f7e88668ccf51dbed6e6e80 Mon Sep 17 00:00:00 2001 From: Daniel Pons Date: Tue, 3 Aug 2021 13:33:35 +0200 Subject: [PATCH 2/2] feat(common): Add support to mock OptimizelyUserContext on unit tests Supports mocking OptimizelyUserContext on frameworks as almost all of them require an interface. --- .../OptimizelySDK.Net35.csproj | 3 + .../OptimizelySDK.Net40.csproj | 3 + .../OptimizelySDK.NetStandard16.csproj | 5 +- .../OptimizelySDK.NetStandard20.csproj | 3 + .../OptimizelyUserContextTest.cs | 14 +-- OptimizelySDK.Tests/TestSetup.cs | 18 ++- OptimizelySDK/IOptimizely.cs | 2 +- OptimizelySDK/IOptimizelyUserContext.cs | 112 ++++++++++++++++++ OptimizelySDK/Optimizely.cs | 8 +- .../OptimizelyDecisions/OptimizelyDecision.cs | 6 +- OptimizelySDK/OptimizelySDK.csproj | 1 + OptimizelySDK/OptimizelyUserContext.cs | 5 +- 12 files changed, 161 insertions(+), 19 deletions(-) create mode 100644 OptimizelySDK/IOptimizelyUserContext.cs diff --git a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj index 8dc64e4e..51db7eb4 100644 --- a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj +++ b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj @@ -148,6 +148,9 @@ IOptimizely.cs + + IOptimizelyUserContext.cs + Logger\DefaultLogger.cs diff --git a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj index e5c64c6c..d451c076 100644 --- a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj +++ b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj @@ -48,6 +48,9 @@ + + IOptimizelyUserContext.cs + AudienceConditions\AndCondition.cs diff --git a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj index 7d5e7c81..5824a1c5 100644 --- a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj +++ b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj @@ -100,7 +100,10 @@ AtomicProjectConfigManager.cs - OptimizelyFactory.cs + OptimizelyFactory.cs + + + IOptimizelyUserContext.cs ConversionEvent.cs diff --git a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj index b43a21cb..63997423 100644 --- a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj +++ b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj @@ -10,6 +10,9 @@ IOptimizely.cs + + IOptimizelyUserContext.cs + Optimizely.cs diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index 6d9630ca..12baae0f 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -63,7 +63,7 @@ public void SetUp() public void OptimizelyUserContextWithAttributes() { var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; - OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); + IOptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); Assert.AreEqual(user.GetOptimizely(), Optimizely); Assert.AreEqual(user.GetUserId(), UserID); @@ -73,7 +73,7 @@ public void OptimizelyUserContextWithAttributes() [Test] public void OptimizelyUserContextNoAttributes() { - OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); + IOptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); Assert.AreEqual(user.GetOptimizely(), Optimizely); Assert.AreEqual(user.GetUserId(), UserID); @@ -84,7 +84,7 @@ public void OptimizelyUserContextNoAttributes() public void SetAttribute() { var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; - OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); + IOptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); user.SetAttribute("k1", "v1"); user.SetAttribute("k2", true); @@ -104,7 +104,7 @@ public void SetAttribute() [Test] public void SetAttributeNoAttribute() { - OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); + IOptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); user.SetAttribute("k1", "v1"); user.SetAttribute("k2", true); @@ -120,7 +120,7 @@ public void SetAttributeNoAttribute() public void SetAttributeOverride() { var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } }; - OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); + IOptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); user.SetAttribute("k1", "v1"); user.SetAttribute("house", "v2"); @@ -134,7 +134,7 @@ public void SetAttributeOverride() public void SetAttributeNullValue() { var attributes = new UserAttributes() { { "k1", null } }; - OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); + IOptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); var newAttributes = user.GetAttributes(); Assert.AreEqual(newAttributes["k1"], null); @@ -151,7 +151,7 @@ public void SetAttributeNullValue() [Test] public void SetAttributeToOverrideAttribute() { - OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); + IOptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object); Assert.AreEqual(user.GetOptimizely(), Optimizely); diff --git a/OptimizelySDK.Tests/TestSetup.cs b/OptimizelySDK.Tests/TestSetup.cs index 5f77494c..7049f1c5 100644 --- a/OptimizelySDK.Tests/TestSetup.cs +++ b/OptimizelySDK.Tests/TestSetup.cs @@ -1,4 +1,20 @@ -using NUnit.Framework; +/* + * Copyright 2021, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using NUnit.Framework; using System.Globalization; using System.Threading; diff --git a/OptimizelySDK/IOptimizely.cs b/OptimizelySDK/IOptimizely.cs index 836cdb92..77643bf1 100644 --- a/OptimizelySDK/IOptimizely.cs +++ b/OptimizelySDK/IOptimizely.cs @@ -41,7 +41,7 @@ public interface IOptimizely /// The user ID to be used for bucketing. /// The user's attributes /// OptimizelyUserContext | An OptimizelyUserContext associated with this OptimizelyClient. - OptimizelyUserContext CreateUserContext(string userId, UserAttributes userAttributes = null); + IOptimizelyUserContext CreateUserContext(string userId, UserAttributes userAttributes = null); /// /// Sends conversion event to Optimizely. diff --git a/OptimizelySDK/IOptimizelyUserContext.cs b/OptimizelySDK/IOptimizelyUserContext.cs new file mode 100644 index 00000000..1111a904 --- /dev/null +++ b/OptimizelySDK/IOptimizelyUserContext.cs @@ -0,0 +1,112 @@ +/* + * Copyright 2020-2021, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using OptimizelySDK.Entity; +using OptimizelySDK.OptimizelyDecisions; +using System.Collections.Generic; + +namespace OptimizelySDK +{ + public interface IOptimizelyUserContext + { + /// + /// Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context, which contains all data required to deliver the flag. + ///
    + ///
  • If the SDK finds an error, it’ll return a decision with null for variationKey. The decision will include an error message in reasons. + ///
+ ///
+ /// A flag key for which a decision will be made. + /// A decision result. + OptimizelyDecision Decide(string key); + + /// + /// Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context, which contains all data required to deliver the flag. + ///
    + ///
  • If the SDK finds an error, it’ll return a decision with null for variationKey. The decision will include an error message in reasons. + ///
+ ///
+ /// A flag key for which a decision will be made. + /// A list of options for decision-making. + /// A decision result. + OptimizelyDecision Decide(string key, OptimizelyDecideOption[] options); + + /// + /// Returns a key-map of decision results ({@link OptimizelyDecision}) for all active flag keys. + /// + /// A dictionary of all decision results, mapped by flag keys. + Dictionary DecideAll(); + + /// + /// Returns a key-map of decision results ({@link OptimizelyDecision}) for all active flag keys. + /// + /// A list of options for decision-making. + /// All decision results mapped by flag keys. + Dictionary DecideAll(OptimizelyDecideOption[] options); + + /// + /// Returns a key-map of decision results for multiple flag keys and a user context. + /// + /// list of flag keys for which a decision will be made. + /// A dictionary of all decision results, mapped by flag keys. + Dictionary DecideForKeys(string[] keys); + + /// + /// Returns a key-map of decision results for multiple flag keys and a user context. + /// + /// list of flag keys for which a decision will be made. + /// An array of decision options. + /// + Dictionary DecideForKeys(string[] keys, OptimizelyDecideOption[] options); + + /// + /// Returns copy of UserAttributes associated with UserContext. + /// + /// copy of UserAttributes. + UserAttributes GetAttributes(); + + /// + /// Returns Optimizely instance associated with the UserContext. + /// + /// Optimizely instance. + Optimizely GetOptimizely(); + + /// + /// Returns UserId associated with the UserContext + /// + /// UserId of this instance. + string GetUserId(); + + /// + /// Set an attribute for a given key. + /// + /// An attribute key + /// value An attribute value + void SetAttribute(string key, object value); + + /// + /// Track an event. + /// + /// The event name. + void TrackEvent(string eventName); + + /// + /// Track an event. + /// + /// The event name. + /// A map of event tag names to event tag values. + void TrackEvent(string eventName, EventTags eventTags); + } +} diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 5aae0da4..e4c362bb 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -700,7 +700,7 @@ public OptimizelyJSON GetFeatureVariableJSON(string featureKey, string variableK /// The user ID to be used for bucketing. /// The user's attributes /// OptimizelyUserContext | An OptimizelyUserContext associated with this OptimizelyClient. - public OptimizelyUserContext CreateUserContext(string userId, + public IOptimizelyUserContext CreateUserContext(string userId, UserAttributes userAttributes = null) { var inputValues = new Dictionary @@ -724,7 +724,7 @@ public OptimizelyUserContext CreateUserContext(string userId, /// A flag key for which a decision will be made. /// A list of options for decision-making. /// A decision result. - internal OptimizelyDecision Decide(OptimizelyUserContext user, + internal OptimizelyDecision Decide(IOptimizelyUserContext user, string key, OptimizelyDecideOption[] options) { @@ -844,7 +844,7 @@ internal OptimizelyDecision Decide(OptimizelyUserContext user, reasonsToReport); } - internal Dictionary DecideAll(OptimizelyUserContext user, + internal Dictionary DecideAll(IOptimizelyUserContext user, OptimizelyDecideOption[] options) { var decisionMap = new Dictionary(); @@ -862,7 +862,7 @@ internal Dictionary DecideAll(OptimizelyUserContext return DecideForKeys(user, allFlagKeys, options); } - internal Dictionary DecideForKeys(OptimizelyUserContext user, + internal Dictionary DecideForKeys(IOptimizelyUserContext user, string[] keys, OptimizelyDecideOption[] options) { diff --git a/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs b/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs index dfc571ac..8afeeec9 100644 --- a/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs +++ b/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs @@ -53,7 +53,7 @@ public class OptimizelyDecision /// /// user context for which the decision was made. /// - public OptimizelyUserContext UserContext { get; private set; } + public IOptimizelyUserContext UserContext { get; private set; } /// /// an array of error/info/debug messages describing why the decision has been made. @@ -65,7 +65,7 @@ public OptimizelyDecision(string variationKey, OptimizelyJSON variables, string ruleKey, string flagKey, - OptimizelyUserContext userContext, + IOptimizelyUserContext userContext, string[] reasons) { VariationKey = variationKey; @@ -84,7 +84,7 @@ public OptimizelyDecision(string variationKey, /// and error reason array /// public static OptimizelyDecision NewErrorDecision(string key, - OptimizelyUserContext optimizelyUserContext, + IOptimizelyUserContext optimizelyUserContext, string error, IErrorHandler errorHandler, ILogger logger) diff --git a/OptimizelySDK/OptimizelySDK.csproj b/OptimizelySDK/OptimizelySDK.csproj index 50f8b137..d3ac9047 100644 --- a/OptimizelySDK/OptimizelySDK.csproj +++ b/OptimizelySDK/OptimizelySDK.csproj @@ -87,6 +87,7 @@ + diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs index aca35788..35aca732 100644 --- a/OptimizelySDK/OptimizelyUserContext.cs +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -25,7 +25,7 @@ namespace OptimizelySDK /// /// OptimizelyUserContext defines user contexts that the SDK will use to make decisions for /// - public class OptimizelyUserContext + public class OptimizelyUserContext : IOptimizelyUserContext { private ILogger Logger; private IErrorHandler ErrorHandler; @@ -46,7 +46,7 @@ public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttribute UserId = userId; } - private OptimizelyUserContext Copy() => new OptimizelyUserContext(Optimizely, UserId, GetAttributes(), ErrorHandler, Logger); + private IOptimizelyUserContext Copy() => new OptimizelyUserContext(Optimizely, UserId, GetAttributes(), ErrorHandler, Logger); /// /// Returns Optimizely instance associated with the UserContext. @@ -133,6 +133,7 @@ public OptimizelyDecision Decide(string key, /// Returns a key-map of decision results for multiple flag keys and a user context. /// /// list of flag keys for which a decision will be made. + /// An array of decision options. /// A dictionary of all decision results, mapped by flag keys. public Dictionary DecideForKeys(string[] keys, OptimizelyDecideOption[] options) {