Skip to content

Commit 9564ea7

Browse files
Add tests for remote data source
1 parent 31e7acb commit 9564ea7

File tree

3 files changed

+223
-1
lines changed

3 files changed

+223
-1
lines changed

app/build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,6 @@ dependencies {
107107
testImplementation libs.androidx.core.testing
108108
testImplementation libs.mockito.core
109109
testImplementation libs.mockito.inline
110-
}
110+
testImplementation libs.robolectric
111+
}
112+
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package com.d4rk.androidtutorials.java.data.source.remote;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertNotNull;
5+
import static org.junit.Assert.assertTrue;
6+
import static org.mockito.ArgumentMatchers.any;
7+
import static org.mockito.Mockito.mock;
8+
import static org.mockito.Mockito.when;
9+
10+
import android.os.Looper;
11+
12+
import com.android.volley.Request;
13+
import com.android.volley.RequestQueue;
14+
import com.android.volley.Response;
15+
import com.android.volley.VolleyError;
16+
import com.android.volley.toolbox.JsonObjectRequest;
17+
import com.d4rk.androidtutorials.java.data.model.PromotedApp;
18+
import com.d4rk.androidtutorials.java.data.source.DefaultHomeRemoteDataSource;
19+
20+
import org.json.JSONArray;
21+
import org.json.JSONException;
22+
import org.json.JSONObject;
23+
import org.junit.Before;
24+
import org.junit.Test;
25+
import org.junit.runner.RunWith;
26+
import org.robolectric.RobolectricTestRunner;
27+
import org.robolectric.Shadows;
28+
29+
import java.lang.reflect.Field;
30+
import java.lang.reflect.Method;
31+
import java.util.List;
32+
import java.util.concurrent.CountDownLatch;
33+
import java.util.concurrent.TimeUnit;
34+
import java.util.concurrent.atomic.AtomicReference;
35+
36+
@RunWith(RobolectricTestRunner.class)
37+
public class DefaultHomeRemoteDataSourceTest {
38+
39+
private RequestQueue requestQueue;
40+
private DefaultHomeRemoteDataSource dataSource;
41+
private AtomicReference<JsonObjectRequest> lastRequest;
42+
43+
@Before
44+
public void setUp() {
45+
requestQueue = mock(RequestQueue.class);
46+
lastRequest = new AtomicReference<>();
47+
when(requestQueue.add(any(Request.class))).thenAnswer(invocation -> {
48+
Request<?> request = invocation.getArgument(0);
49+
if (request instanceof JsonObjectRequest) {
50+
lastRequest.set((JsonObjectRequest) request);
51+
}
52+
return request;
53+
});
54+
dataSource = new DefaultHomeRemoteDataSource(requestQueue, "https://example.com/api");
55+
}
56+
57+
@Test
58+
public void fetchPromotedApps_withValidResponse_deliversParsedAppsOnMainThread() throws Exception {
59+
CountDownLatch latch = new CountDownLatch(1);
60+
AtomicReference<List<PromotedApp>> resultRef = new AtomicReference<>();
61+
AtomicReference<Thread> threadRef = new AtomicReference<>();
62+
63+
dataSource.fetchPromotedApps(apps -> {
64+
resultRef.set(apps);
65+
threadRef.set(Thread.currentThread());
66+
latch.countDown();
67+
});
68+
69+
JsonObjectRequest request = captureLastRequest();
70+
deliverSuccess(request, createValidResponse());
71+
72+
assertTrue("Callback not invoked", waitForCallback(latch));
73+
74+
List<PromotedApp> apps = resultRef.get();
75+
assertNotNull(apps);
76+
assertEquals(1, apps.size());
77+
78+
PromotedApp promotedApp = apps.get(0);
79+
assertEquals("Example App", promotedApp.name());
80+
assertEquals("com.example.promoted", promotedApp.packageName());
81+
assertEquals("https://example.com/icon.png", promotedApp.iconUrl());
82+
83+
assertEquals(Looper.getMainLooper().getThread(), threadRef.get());
84+
}
85+
86+
@Test
87+
public void fetchPromotedApps_withMalformedResponse_returnsEmptyListOnMainThread() throws Exception {
88+
CountDownLatch latch = new CountDownLatch(1);
89+
AtomicReference<List<PromotedApp>> resultRef = new AtomicReference<>();
90+
AtomicReference<Thread> threadRef = new AtomicReference<>();
91+
92+
dataSource.fetchPromotedApps(apps -> {
93+
resultRef.set(apps);
94+
threadRef.set(Thread.currentThread());
95+
latch.countDown();
96+
});
97+
98+
JsonObjectRequest request = captureLastRequest();
99+
deliverSuccess(request, createMalformedResponse());
100+
101+
assertTrue("Callback not invoked", waitForCallback(latch));
102+
103+
List<PromotedApp> apps = resultRef.get();
104+
assertNotNull(apps);
105+
assertTrue(apps.isEmpty());
106+
assertEquals(Looper.getMainLooper().getThread(), threadRef.get());
107+
}
108+
109+
@Test
110+
public void fetchPromotedApps_onNetworkError_returnsEmptyListOnMainThread() throws Exception {
111+
CountDownLatch latch = new CountDownLatch(1);
112+
AtomicReference<List<PromotedApp>> resultRef = new AtomicReference<>();
113+
AtomicReference<Thread> threadRef = new AtomicReference<>();
114+
115+
dataSource.fetchPromotedApps(apps -> {
116+
resultRef.set(apps);
117+
threadRef.set(Thread.currentThread());
118+
latch.countDown();
119+
});
120+
121+
JsonObjectRequest request = captureLastRequest();
122+
deliverError(request, new VolleyError("network"));
123+
124+
assertTrue("Callback not invoked", waitForCallback(latch));
125+
126+
List<PromotedApp> apps = resultRef.get();
127+
assertNotNull(apps);
128+
assertTrue(apps.isEmpty());
129+
assertEquals(Looper.getMainLooper().getThread(), threadRef.get());
130+
}
131+
132+
@Test
133+
public void parseResponse_skipsTutorialPackages() throws Exception {
134+
JSONObject response = createValidResponse();
135+
Method parseResponse = DefaultHomeRemoteDataSource.class
136+
.getDeclaredMethod("parseResponse", JSONObject.class);
137+
parseResponse.setAccessible(true);
138+
139+
@SuppressWarnings("unchecked")
140+
List<PromotedApp> apps = (List<PromotedApp>) parseResponse.invoke(dataSource, response);
141+
assertEquals(1, apps.size());
142+
assertEquals("com.example.promoted", apps.get(0).packageName());
143+
}
144+
145+
private JsonObjectRequest captureLastRequest() {
146+
JsonObjectRequest request = lastRequest.get();
147+
assertNotNull("Expected a JsonObjectRequest to be enqueued", request);
148+
return request;
149+
}
150+
151+
private void deliverSuccess(JsonObjectRequest request, JSONObject response) throws Exception {
152+
@SuppressWarnings("unchecked")
153+
Response.Listener<JSONObject> listener = (Response.Listener<JSONObject>) getFieldValue(request, "mListener");
154+
assertNotNull(listener);
155+
listener.onResponse(response);
156+
}
157+
158+
private void deliverError(JsonObjectRequest request, VolleyError error) throws Exception {
159+
Response.ErrorListener listener = (Response.ErrorListener) getFieldValue(request, "mErrorListener");
160+
assertNotNull(listener);
161+
listener.onErrorResponse(error);
162+
}
163+
164+
private Object getFieldValue(Object target, String fieldName) throws Exception {
165+
Class<?> type = target.getClass();
166+
while (type != null) {
167+
try {
168+
Field field = type.getDeclaredField(fieldName);
169+
field.setAccessible(true);
170+
return field.get(target);
171+
} catch (NoSuchFieldException ignored) {
172+
type = type.getSuperclass();
173+
}
174+
}
175+
throw new NoSuchFieldException(fieldName);
176+
}
177+
178+
private boolean waitForCallback(CountDownLatch latch) throws InterruptedException {
179+
long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(1);
180+
while (latch.getCount() > 0 && System.currentTimeMillis() < deadline) {
181+
Shadows.shadowOf(Looper.getMainLooper()).idle();
182+
Thread.sleep(1);
183+
}
184+
return latch.getCount() == 0;
185+
}
186+
187+
private static JSONObject createValidResponse() throws JSONException {
188+
JSONObject first = new JSONObject();
189+
first.put("name", "Example App");
190+
first.put("packageName", "com.example.promoted");
191+
first.put("iconLogo", "https://example.com/icon.png");
192+
193+
JSONObject blocked = new JSONObject();
194+
blocked.put("name", "Tutorial App");
195+
blocked.put("packageName", "com.d4rk.androidtutorials");
196+
blocked.put("iconLogo", "https://example.com/tutorial.png");
197+
198+
JSONArray apps = new JSONArray();
199+
apps.put(first);
200+
apps.put(blocked);
201+
202+
JSONObject data = new JSONObject();
203+
data.put("apps", apps);
204+
205+
JSONObject response = new JSONObject();
206+
response.put("data", data);
207+
return response;
208+
}
209+
210+
private static JSONObject createMalformedResponse() throws JSONException {
211+
JSONObject data = new JSONObject();
212+
data.put("invalid", new JSONArray());
213+
214+
JSONObject response = new JSONObject();
215+
response.put("data", data);
216+
return response;
217+
}
218+
}

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ hilt = "2.57.1"
3030
room = "2.8.0"
3131
glide = "5.0.4"
3232
retrofit = "3.0.0"
33+
robolectric = "4.13.2"
3334

3435
[libraries]
3536
aboutlibraries = { module = "com.mikepenz:aboutlibraries", version.ref = "aboutlibraries" }
@@ -73,3 +74,4 @@ glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
7374
glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" }
7475
retrofit2 = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
7576
retrofit2-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
77+
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }

0 commit comments

Comments
 (0)