Skip to content

Commit 2925eb2

Browse files
committed
Test all models populated with random data
* Implement custom Assert that uses reflection on fields (Fest will only work if setter and getter methods are present) * Fix marshalling in Album
1 parent 3e9bd9c commit 2925eb2

6 files changed

Lines changed: 271 additions & 8 deletions

File tree

spotify-api/src/main/java/kaaes/spotify/webapi/android/models/Album.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public int describeContents() {
2727

2828
@Override
2929
public void writeToParcel(Parcel dest, int flags) {
30+
super.writeToParcel(dest, flags);
3031
dest.writeTypedList(artists);
3132
dest.writeTypedList(copyrights);
3233
dest.writeMap(this.external_ids);
@@ -41,6 +42,7 @@ public Album() {
4142
}
4243

4344
protected Album(Parcel in) {
45+
super(in);
4446
this.artists = in.createTypedArrayList(ArtistSimple.CREATOR);
4547
this.copyrights = in.createTypedArrayList(Copyright.CREATOR);
4648
this.external_ids = in.readHashMap(ClassLoader.getSystemClassLoader());

spotify-api/src/main/java/kaaes/spotify/webapi/android/models/Playlist.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ public void writeToParcel(Parcel dest, int flags) {
2525
}
2626

2727
public Playlist() {
28-
super();
2928
}
3029

3130
protected Playlist(Parcel in) {

spotify-api/src/main/java/kaaes/spotify/webapi/android/models/PlaylistSimple.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ public void writeToParcel(Parcel dest, int flags) {
2020
}
2121

2222
public PlaylistSimple() {
23-
super();
2423
}
2524

2625
protected PlaylistSimple(Parcel in) {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package kaaes.spotify.webapi.android;
2+
3+
import android.os.Parcelable;
4+
5+
import org.fest.assertions.api.AbstractAssert;
6+
import org.fest.assertions.api.Assertions;
7+
8+
import java.lang.reflect.Field;
9+
import java.util.List;
10+
11+
public class ModelAssert extends AbstractAssert<ModelAssert, Parcelable> {
12+
13+
public static final String ERROR_MESSAGE = "\nExpected : <%s> \nActual : <%s> \nat %s";
14+
15+
protected ModelAssert(Parcelable actual, Class<?> selfType) {
16+
super(actual, selfType);
17+
}
18+
19+
public static ModelAssert assertThat(Parcelable actual) {
20+
return new ModelAssert(actual, ModelAssert.class);
21+
}
22+
23+
public ModelAssert isEqualByComparingFields(Parcelable expected) {
24+
25+
Field[] fields = expected.getClass().getFields();
26+
for (Field field : fields) {
27+
try {
28+
String fieldName = expected.getClass().getSimpleName() + "#" + field.getName();
29+
30+
Object actualField = field.get(actual);
31+
Object expectedField = field.get(expected);
32+
33+
if (isList(field)) {
34+
compareLists(fieldName, (List) actualField, (List) expectedField);
35+
} else {
36+
37+
// The maps in current models only contain simple types so we skip any fancy
38+
// comparisons and use regular compare
39+
40+
compareFields(fieldName, expectedField, actualField);
41+
}
42+
43+
} catch (IllegalAccessException e) {
44+
throw new AssertionError("Can't access fields");
45+
}
46+
}
47+
48+
return this;
49+
}
50+
51+
private void compareLists(String fieldName, List actual, List expected) {
52+
if (actual == null || expected == null) {
53+
compareFields(fieldName, expected, actual);
54+
return;
55+
}
56+
57+
for (int i = 0; i < actual.size(); i++) {
58+
compareFields(fieldName, expected.get(i), actual.get(i));
59+
}
60+
}
61+
62+
private boolean isList(Field field) {
63+
return List.class.isAssignableFrom(field.getType());
64+
}
65+
66+
private AbstractAssert compareFields(String fieldName, Object expected, Object actual) {
67+
if (actual instanceof Parcelable) {
68+
return ModelAssert.assertThat((Parcelable) actual)
69+
.isEqualByComparingFields((Parcelable) expected);
70+
}
71+
72+
// Be nice and show which field in which class is failing
73+
String fieldPath = this.actual.getClass().getSimpleName() + "#" + fieldName;
74+
75+
return Assertions.assertThat(actual)
76+
.overridingErrorMessage(ERROR_MESSAGE, expected, actual, fieldPath)
77+
.isEqualTo(expected);
78+
}
79+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package kaaes.spotify.webapi.android;
2+
3+
import android.os.Parcelable;
4+
5+
import java.lang.reflect.Field;
6+
import java.lang.reflect.ParameterizedType;
7+
import java.lang.reflect.Type;
8+
import java.util.ArrayList;
9+
import java.util.Arrays;
10+
import java.util.HashMap;
11+
import java.util.List;
12+
import java.util.Map;
13+
import java.util.Random;
14+
15+
/*
16+
* Inspired by the code in the Random Beans project which looks
17+
* cool but would be a bit of an overkill for these tests.
18+
* Random Beans repository URL: https://github.com/benas/random-beans
19+
*/
20+
21+
public class ModelPopulator {
22+
23+
public static final int DEFAULT_STRING_LENGTH = 10;
24+
public static final int DEFAULT_COLLECTION_SIZE = 5;
25+
26+
public static final Random RANDOM = new Random();
27+
public static final Class<String> DEFAULT_GENERIC_CLASS = String.class;
28+
private final List<String> mExcludeFields;
29+
30+
public static class PopulationException extends RuntimeException {
31+
public PopulationException(String details, Throwable throwable) {
32+
super(details, throwable);
33+
}
34+
}
35+
36+
public ModelPopulator(String... excludeFields) {
37+
mExcludeFields = new ArrayList<>();
38+
for (String field : excludeFields) {
39+
mExcludeFields.add(field.toLowerCase());
40+
}
41+
}
42+
43+
public <T> T populateWithRandomValues(final Class<T> type) {
44+
try {
45+
T instance = type.newInstance();
46+
47+
ArrayList<Field> fields = new ArrayList<>(Arrays.asList(type.getDeclaredFields()));
48+
fields.addAll(getInheritedFields(type));
49+
50+
for (Field field : fields) {
51+
52+
if (shouldSkipField(field)) {
53+
continue;
54+
}
55+
56+
if (isCollectionType(field.getType())) {
57+
field.set(instance, populateCollectionField(field));
58+
} else {
59+
field.set(instance, getRandomValueOfType(field.getType()));
60+
}
61+
}
62+
63+
return instance;
64+
} catch (Exception e) {
65+
throw new PopulationException("Populating object failed", e);
66+
}
67+
}
68+
69+
private boolean shouldSkipField(Field field) {
70+
return mExcludeFields.contains(field.getName().toLowerCase());
71+
}
72+
73+
private List<Field> getInheritedFields(Class type) {
74+
List<Field> inheritedFields = new ArrayList<>();
75+
while (type.getSuperclass() != null) {
76+
Class superclass = type.getSuperclass();
77+
inheritedFields.addAll(Arrays.asList(superclass.getDeclaredFields()));
78+
type = superclass;
79+
}
80+
return inheritedFields;
81+
}
82+
83+
private boolean isCollectionType(Class type) {
84+
return Map.class.isAssignableFrom(type) || List.class.isAssignableFrom(type);
85+
}
86+
87+
private Object populateCollectionField(Field field) {
88+
89+
Class type = field.getType();
90+
91+
/* List */
92+
if (List.class.isAssignableFrom(type)) {
93+
Type genericType = field.getGenericType();
94+
ParameterizedType pt = (ParameterizedType) genericType;
95+
Type actualType = pt.getActualTypeArguments()[0];
96+
Class elementClass;
97+
98+
if (actualType instanceof Class) {
99+
elementClass = (Class) actualType;
100+
} else {
101+
// Lists with generics will be populated by default type
102+
elementClass = DEFAULT_GENERIC_CLASS;
103+
}
104+
105+
List list = new ArrayList();
106+
for (int i = 0; i < DEFAULT_COLLECTION_SIZE; i++) {
107+
list.add(getRandomValueOfType(elementClass));
108+
}
109+
return list;
110+
}
111+
112+
/* Map */
113+
if (Map.class.isAssignableFrom(type)) {
114+
Type genericType = field.getGenericType();
115+
ParameterizedType pt = (ParameterizedType) genericType;
116+
Type[] arguments = pt.getActualTypeArguments();
117+
118+
Type key = arguments[0];
119+
Type value = arguments[1];
120+
121+
Class keyClass;
122+
if (key instanceof Class) {
123+
keyClass = (Class) key;
124+
} else {
125+
// Maps with generics will be populated by default type
126+
keyClass = DEFAULT_GENERIC_CLASS;
127+
}
128+
129+
Class valueClass;
130+
if (value instanceof Class) {
131+
valueClass = (Class) value;
132+
} else {
133+
// Maps with generics will be populated by default type
134+
valueClass = DEFAULT_GENERIC_CLASS;
135+
}
136+
137+
Map map = new HashMap();
138+
for (int i = 0; i < DEFAULT_COLLECTION_SIZE; i++) {
139+
map.put(getRandomValueOfType(keyClass), getRandomValueOfType(valueClass));
140+
}
141+
return map;
142+
}
143+
144+
throw new UnsupportedOperationException("Unsupported collection field type! " + type);
145+
}
146+
147+
private Object getRandomValueOfType(Class type) {
148+
149+
/* Another model */
150+
if (Parcelable.class.isAssignableFrom(type)) {
151+
return populateWithRandomValues(type);
152+
}
153+
154+
/* String */
155+
if (type.equals(String.class)) {
156+
StringBuilder builder = new StringBuilder();
157+
for (int i = 0; i < DEFAULT_STRING_LENGTH; i++) {
158+
builder.append((char) (RANDOM.nextInt(26) + 'a'));
159+
}
160+
return builder.toString();
161+
}
162+
163+
/* Integer */
164+
if (type.equals(Integer.TYPE) || type.equals(Integer.class)) {
165+
return RANDOM.nextInt();
166+
}
167+
168+
/* Long */
169+
if (type.equals(Long.TYPE) || type.equals(Long.class)) {
170+
return RANDOM.nextLong();
171+
}
172+
173+
/* Boolean */
174+
if (type.equals(Boolean.TYPE) || type.equals(Boolean.class)) {
175+
return RANDOM.nextBoolean();
176+
}
177+
178+
throw new UnsupportedOperationException("Unsupported field type! " + type);
179+
}
180+
}

spotify-api/src/test/java/kaaes/spotify/webapi/android/ParcelableModelsTest.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,19 @@ public class ParcelableModelsTest {
5858

5959
@Test
6060
public void allParcelables() throws IllegalAccessException, InstantiationException, NoSuchFieldException {
61+
62+
ModelPopulator populator = new ModelPopulator("CREATOR", "$jacocoData");
63+
6164
for (Class<? extends Parcelable> modelClass : getModelClasses()) {
62-
// TODO(dima) instantiate with random fields
63-
Parcelable instance = modelClass.newInstance();
65+
66+
Parcelable instance = populator.populateWithRandomValues(modelClass);
67+
6468
testSingleParcelable(instance);
6569
testParcelableArray(instance);
6670

6771
/* Trick to increase code coverage */
6872
instance.describeContents();
69-
((Parcelable.Creator<?>)modelClass.getField("CREATOR").get(null)).newArray(13);
73+
((Parcelable.Creator<?>) modelClass.getField("CREATOR").get(null)).newArray(13);
7074
}
7175
}
7276

@@ -119,7 +123,7 @@ <T extends Parcelable> void testSingleParcelable(T underTest) {
119123
parcel.setDataPosition(0);
120124
T fromParcel = parcel.readParcelable(underTest.getClass().getClassLoader());
121125

122-
assertThat(fromParcel).isEqualsToByComparingFields(underTest);
126+
ModelAssert.assertThat(fromParcel).isEqualByComparingFields(underTest);
123127
}
124128

125129
<T extends Parcelable> void testParcelableArray(T underTest) {
@@ -158,11 +162,11 @@ public void tracksAreGoodParcelableCitizen() {
158162
@Test
159163
public void usersAreGoodParcelableCitizens() {
160164
String body = TestUtils.readTestData("user.json");
161-
UserPublic userPublic = new GsonBuilder().create().fromJson(body, UserPublic.class);
165+
UserPublic userPublic = new GsonBuilder().create().fromJson(body, UserPublic.class);
162166
testSingleParcelable(userPublic);
163167

164168
body = TestUtils.readTestData("current-user.json");
165-
UserPublic userPrivate = new GsonBuilder().create().fromJson(body, UserPublic.class);
169+
UserPrivate userPrivate = new GsonBuilder().create().fromJson(body, UserPrivate.class);
166170
testSingleParcelable(userPrivate);
167171
}
168172

0 commit comments

Comments
 (0)