Skip to content

Commit 70a3039

Browse files
committed
fix cmab tests
1 parent 4faabd2 commit 70a3039

File tree

4 files changed

+111
-83
lines changed

4 files changed

+111
-83
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2025, Optimizely, Inc. and contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.optimizely.ab.android.sdk.cmab
16+
17+
import com.optimizely.ab.cmab.client.CmabClientHelper
18+
19+
open class CmabClientHelperAndroid {
20+
21+
open val cmabPredictionEndpoint: String
22+
get() = CmabClientHelper.CMAB_PREDICTION_ENDPOINT
23+
24+
open val cmabFetchFailed: String
25+
get() = CmabClientHelper.CMAB_FETCH_FAILED
26+
27+
open val invalidCmabFetchResponse: String
28+
get() = CmabClientHelper.INVALID_CMAB_FETCH_RESPONSE
29+
30+
open fun buildRequestJson(
31+
userId: String?,
32+
ruleId: String?,
33+
attributes: Map<String?, Any?>?,
34+
cmabUuid: String?
35+
): String {
36+
return CmabClientHelper.buildRequestJson(userId, ruleId, attributes, cmabUuid)
37+
}
38+
39+
open fun parseVariationId(jsonResponse: String?): String? {
40+
return CmabClientHelper.parseVariationId(jsonResponse)
41+
}
42+
43+
open fun validateResponse(responseBody: String?): Boolean {
44+
return CmabClientHelper.validateResponse(responseBody)
45+
}
46+
47+
open fun isSuccessStatusCode(statusCode: Int): Boolean {
48+
return CmabClientHelper.isSuccessStatusCode(statusCode)
49+
}
50+
}

android-sdk/src/main/java/com/optimizely/ab/android/sdk/cmab/DefaultCmabClient.kt

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,33 @@ import androidx.annotation.VisibleForTesting
1919
import com.optimizely.ab.android.shared.Client
2020
import com.optimizely.ab.android.shared.OptlyStorage
2121
import com.optimizely.ab.cmab.client.CmabClient
22-
import com.optimizely.ab.cmab.client.CmabClientHelper
2322
import com.optimizely.ab.cmab.client.CmabFetchException
2423
import com.optimizely.ab.cmab.client.CmabInvalidResponseException
2524
import org.slf4j.LoggerFactory
2625
import java.net.HttpURLConnection
2726
import java.net.URL
2827

2928
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
30-
open class DefaultCmabClient(private val client: Client) : CmabClient {
31-
29+
open class DefaultCmabClient : CmabClient {
30+
private val client: Client
31+
private val cmabClientHelper: CmabClientHelperAndroid
3232
private val logger = LoggerFactory.getLogger(DefaultCmabClient::class.java)
3333

34-
constructor(context: Context) : this(Client(OptlyStorage(context), LoggerFactory.getLogger(OptlyStorage::class.java)))
34+
constructor(context: Context) {
35+
this.client =
36+
Client(OptlyStorage(context), LoggerFactory.getLogger(OptlyStorage::class.java))
37+
this.cmabClientHelper = CmabClientHelperAndroid()
38+
}
39+
40+
constructor(client: Client) {
41+
this.client = client
42+
this.cmabClientHelper = CmabClientHelperAndroid()
43+
}
44+
45+
constructor(client: Client, cmabClientHelper: CmabClientHelperAndroid) {
46+
this.client = client
47+
this.cmabClientHelper = cmabClientHelper
48+
}
3549

3650
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
3751
override fun fetchDecision(
@@ -43,10 +57,10 @@ open class DefaultCmabClient(private val client: Client) : CmabClient {
4357
val request: Client.Request<String?> = Client.Request {
4458
var urlConnection: HttpURLConnection? = null
4559
try {
46-
val apiEndpoint = String.format(CmabClientHelper.CMAB_PREDICTION_ENDPOINT, ruleId)
60+
val apiEndpoint = String.format(cmabClientHelper.cmabPredictionEndpoint, ruleId)
4761

4862
val requestBody: String =
49-
CmabClientHelper.buildRequestJson(userId, ruleId, attributes, cmabUuid)
63+
cmabClientHelper.buildRequestJson(userId, ruleId, attributes, cmabUuid)
5064

5165
val url = URL(apiEndpoint)
5266
urlConnection = client.openConnection(url)
@@ -72,22 +86,23 @@ open class DefaultCmabClient(private val client: Client) : CmabClient {
7286
val json = client.readStream(urlConnection)
7387
logger.debug("Successfully fetched CMAB decision: {}", json)
7488

75-
if (!CmabClientHelper.validateResponse(json)) {
76-
logger.error(CmabClientHelper.INVALID_CMAB_FETCH_RESPONSE)
77-
throw CmabInvalidResponseException(CmabClientHelper.INVALID_CMAB_FETCH_RESPONSE)
89+
if (!cmabClientHelper.validateResponse(json)) {
90+
logger.error(cmabClientHelper.invalidCmabFetchResponse)
91+
throw CmabInvalidResponseException(cmabClientHelper.invalidCmabFetchResponse)
7892
}
79-
return@Request CmabClientHelper.parseVariationId(json)
93+
94+
return@Request cmabClientHelper.parseVariationId(json)
8095
} else {
8196
val errorMessage: String = java.lang.String.format(
82-
CmabClientHelper.CMAB_FETCH_FAILED,
97+
cmabClientHelper.cmabFetchFailed,
8398
urlConnection.responseMessage
8499
)
85100
logger.error(errorMessage)
86101
throw CmabFetchException(errorMessage)
87102
}
88103
} catch (e: Exception) {
89104
val errorMessage: String =
90-
java.lang.String.format(CmabClientHelper.CMAB_FETCH_FAILED, e.message)
105+
java.lang.String.format(cmabClientHelper.cmabFetchFailed, e.message)
91106
logger.error(errorMessage)
92107
throw CmabFetchException(errorMessage)
93108
} finally {

android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyUserContextAndroidTest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626
import org.junit.Test;
2727
import org.junit.runner.RunWith;
2828
import org.mockito.Mock;
29+
import org.mockito.Mockito;
2930
import org.mockito.junit.MockitoJUnitRunner;
3031
import org.powermock.api.mockito.PowerMockito;
3132

33+
import java.lang.reflect.Method;
3234
import java.util.Arrays;
3335
import java.util.Collections;
3436
import java.util.HashMap;
@@ -39,6 +41,7 @@
3941
import static org.junit.Assert.assertNotNull;
4042
import static org.mockito.ArgumentMatchers.any;
4143
import static org.mockito.ArgumentMatchers.eq;
44+
import static org.mockito.Mockito.doReturn;
4245
import static org.mockito.Mockito.spy;
4346
import static org.mockito.Mockito.verify;
4447
import static org.mockito.Mockito.when;
@@ -226,7 +229,7 @@ public void testDecideAll_withoutOptions() throws Exception {
226229
PowerMockito.doReturn(mockDecisionsMap).when(userContextSpy, "decideAllSync", any());
227230

228231
Map<String, OptimizelyDecision> result = userContextSpy.decideAll();
229-
verify((OptimizelyUserContext)userContextSpy).decideAllSync(eq(Collections.emptyList()));
232+
verify(userContextSpy).decideAllSync(eq(Collections.emptyList()));
230233
assertEquals(mockDecisionsMap, result);
231234
}
232235

android-sdk/src/test/java/com/optimizely/ab/android/sdk/cmab/DefaultCmabClientTest.java

Lines changed: 30 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,12 @@
1515
package com.optimizely.ab.android.sdk.cmab;
1616

1717
import com.optimizely.ab.android.shared.Client;
18-
import com.optimizely.ab.cmab.client.CmabClientHelper;
1918

2019
import org.junit.Before;
2120
import org.junit.Test;
2221
import org.junit.runner.RunWith;
2322
import org.mockito.ArgumentCaptor;
24-
import org.powermock.api.mockito.PowerMockito;
25-
import org.powermock.core.classloader.annotations.PowerMockIgnore;
26-
import org.powermock.core.classloader.annotations.PrepareForTest;
27-
import org.powermock.modules.junit4.PowerMockRunner;
23+
import org.mockito.junit.MockitoJUnitRunner;
2824
import org.slf4j.Logger;
2925

3026
import java.io.ByteArrayOutputStream;
@@ -37,23 +33,16 @@
3733
import static org.junit.Assert.*;
3834
import static org.mockito.ArgumentMatchers.*;
3935
import static org.mockito.Mockito.*;
40-
import static org.powermock.api.mockito.PowerMockito.mockStatic;
41-
import static org.powermock.api.mockito.PowerMockito.when;
4236

4337
/**
4438
* Tests for {@link DefaultCmabClient}
4539
*/
46-
@RunWith(PowerMockRunner.class)
47-
@PrepareForTest({CmabClientHelper.class})
48-
@PowerMockIgnore({"javax.net.ssl.*", "javax.security.*"})
40+
@RunWith(MockitoJUnitRunner.class)
4941
public class DefaultCmabClientTest {
5042

5143
private Client mockClient;
52-
private Logger mockLogger;
53-
private HttpURLConnection mockUrlConnection;
54-
private ByteArrayOutputStream mockOutputStream;
55-
56-
private DefaultCmabClient cmabClient;
44+
private CmabClientHelperAndroid mockCmabClientHelper;
45+
private DefaultCmabClient mockCmabClient;
5746
private String testRuleId = "test-rule-123";
5847
private String testUserId = "test-user-456";
5948
private String testCmabUuid = "test-uuid-789";
@@ -62,101 +51,72 @@ public class DefaultCmabClientTest {
6251
@Before
6352
public void setup() {
6453
mockClient = mock(Client.class);
65-
mockLogger = mock(Logger.class);
66-
mockUrlConnection = mock(HttpURLConnection.class);
67-
mockOutputStream = mock(ByteArrayOutputStream.class);
68-
69-
cmabClient = new DefaultCmabClient(mockClient);
54+
mockCmabClientHelper = spy(new CmabClientHelperAndroid());
7055

7156
testAttributes = new HashMap<>();
7257
testAttributes.put("age", 25);
7358
testAttributes.put("country", "US");
74-
75-
// Mock static methods
76-
mockStatic(CmabClientHelper.class);
7759
}
7860

7961
@Test
8062
public void testFetchDecisionSuccess() throws Exception {
81-
// Mock successful HTTP response
63+
HttpURLConnection mockUrlConnection = mock(HttpURLConnection.class);
64+
ByteArrayOutputStream mockOutputStream = mock(ByteArrayOutputStream.class);
65+
8266
String mockResponseJson = "{\"variation_id\":\"variation_1\",\"status\":\"success\"}";
8367
when(mockClient.openConnection(any(URL.class))).thenReturn(mockUrlConnection);
8468
when(mockUrlConnection.getResponseCode()).thenReturn(200);
8569
when(mockClient.readStream(mockUrlConnection)).thenReturn(mockResponseJson);
8670
when(mockUrlConnection.getOutputStream()).thenReturn(mockOutputStream);
87-
when(mockClient.execute(any(Client.Request.class), eq(2), eq(2))).thenAnswer(invocation -> {
71+
when(mockClient.execute(any(Client.Request.class), anyInt(), anyInt())).thenAnswer(invocation -> {
8872
Client.Request<String> request = invocation.getArgument(0);
8973
return request.execute();
9074
});
9175

92-
// Mock the helper methods
93-
when(CmabClientHelper.buildRequestJson(testUserId, testRuleId, testAttributes, testCmabUuid))
94-
.thenReturn("{\"user_id\":\"test-user-456\"}");
95-
when(CmabClientHelper.validateResponse(mockResponseJson)).thenReturn(true);
96-
when(CmabClientHelper.parseVariationId(mockResponseJson)).thenReturn("variation_1");
76+
doReturn("{\"user_id\":\"test-user-456\"}")
77+
.when(mockCmabClientHelper)
78+
.buildRequestJson(any(), any(), any(), any());
79+
doReturn(true)
80+
.when(mockCmabClientHelper)
81+
.validateResponse(any());
82+
doReturn("variation_1")
83+
.when(mockCmabClientHelper)
84+
.parseVariationId(any());
85+
86+
mockCmabClient = new DefaultCmabClient(mockClient, mockCmabClientHelper);
9787

98-
String result = cmabClient.fetchDecision(testRuleId, testUserId, testAttributes, testCmabUuid);
88+
String result = mockCmabClient.fetchDecision(testRuleId, testUserId, testAttributes, testCmabUuid);
9989

10090
assertEquals("variation_1", result);
101-
verify(mockUrlConnection).setConnectTimeout(10000);
102-
verify(mockUrlConnection).setReadTimeout(60000);
91+
92+
verify(mockUrlConnection).setConnectTimeout(10*1000);
93+
verify(mockUrlConnection).setReadTimeout(60*1000);
10394
verify(mockUrlConnection).setRequestMethod("POST");
10495
verify(mockUrlConnection).setRequestProperty("content-type", "application/json");
10596
verify(mockUrlConnection).setDoOutput(true);
106-
verify(mockLogger).debug("Successfully fetched CMAB decision: {}", mockResponseJson);
10797
}
10898

10999
@Test
110100
public void testFetchDecisionConnectionFailure() throws Exception {
111-
// Mock connection failure
112101
when(mockClient.openConnection(any(URL.class))).thenReturn(null);
113-
when(mockClient.execute(any(Client.Request.class), eq(2), eq(2))).thenAnswer(invocation -> {
114-
Client.Request<String> request = invocation.getArgument(0);
115-
return request.execute();
116-
});
117-
118-
// Mock the helper methods
119-
when(CmabClientHelper.buildRequestJson(testUserId, testRuleId, testAttributes, testCmabUuid))
120-
.thenReturn("{\"user_id\":\"test-user-456\"}");
121-
122-
String result = cmabClient.fetchDecision(testRuleId, testUserId, testAttributes, testCmabUuid);
123-
124-
assertNull(result);
125-
}
126-
127-
@Test
128-
public void testConnectionTimeouts() throws Exception {
129-
when(mockClient.openConnection(any(URL.class))).thenReturn(mockUrlConnection);
130-
when(mockUrlConnection.getResponseCode()).thenReturn(200);
131-
when(mockClient.readStream(mockUrlConnection)).thenReturn("{\"variation_id\":\"test\"}");
132-
when(mockUrlConnection.getOutputStream()).thenReturn(mockOutputStream);
133102
when(mockClient.execute(any(Client.Request.class), anyInt(), anyInt())).thenAnswer(invocation -> {
134103
Client.Request<String> request = invocation.getArgument(0);
135104
return request.execute();
136105
});
137106

138-
when(CmabClientHelper.buildRequestJson(any(), any(), any(), any())).thenReturn("{}");
139-
when(CmabClientHelper.validateResponse(any())).thenReturn(true);
140-
when(CmabClientHelper.parseVariationId(any())).thenReturn("test");
141-
142-
cmabClient.fetchDecision(testRuleId, testUserId, testAttributes, testCmabUuid);
107+
mockCmabClient = new DefaultCmabClient(mockClient, mockCmabClientHelper);
143108

144-
verify(mockUrlConnection).setConnectTimeout(10 * 1000); // 10 seconds
145-
verify(mockUrlConnection).setReadTimeout(60 * 1000); // 60 seconds
109+
String result = mockCmabClient.fetchDecision(testRuleId, testUserId, testAttributes, testCmabUuid);
110+
assertNull(result);
146111
}
147112

148113
@Test
149114
public void testRetryOnFailureWithRetryBackoff() throws Exception {
150-
when(mockClient.openConnection(any(URL.class))).thenReturn(mockUrlConnection);
151-
when(mockUrlConnection.getResponseCode()).thenReturn(500);
152-
when(mockUrlConnection.getResponseMessage()).thenReturn("Server Error");
153-
when(mockUrlConnection.getOutputStream()).thenReturn(mockOutputStream);
154-
155-
when(mockClient.execute(any(Client.Request.class), eq(1), eq(2))).thenReturn(null);
115+
when(mockClient.execute(any(Client.Request.class), anyInt(), anyInt())).thenReturn(null);
156116

157-
when(CmabClientHelper.buildRequestJson(any(), any(), any(), any())).thenReturn("{}");
117+
mockCmabClient = new DefaultCmabClient(mockClient, mockCmabClientHelper);
158118

159-
String result = cmabClient.fetchDecision(testRuleId, testUserId, testAttributes, testCmabUuid);
119+
String result = mockCmabClient.fetchDecision(testRuleId, testUserId, testAttributes, testCmabUuid);
160120
assertNull(result);
161121

162122
// Verify the retry configuration matches our constants

0 commit comments

Comments
 (0)