Skip to content

Commit 5be13fe

Browse files
authored
[Widget] Added unit tests and instrumentation tests to the Widget Sample (#40)
* [Widget] Added unit tests and instrumentation tests to the Widget Sample, split logic from network and parsing - Refactored tasks.withType(KotlinCompile) to be included in allprojects so it will work also for tests - Separated the networking logic from the parsing logic using NetworkFeed object - Moved rssItems data to the WidgetFactory to use the object's methods just as helper functions - Created a BaseSimpleApi interface so RssSimpleApi can be mocked with a test api - Fixed stripHtml special character regex - Added unit tests for networking logic with a mocked web server - Added UI tests for the changing the feed url - Added instrumentation tests for the xml parsing
1 parent d159eb3 commit 5be13fe

File tree

12 files changed

+608
-119
lines changed

12 files changed

+608
-119
lines changed

Widget/app/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ dependencies {
3131
implementation networkDependencies.okhttpLoggingInterceptor
3232

3333
testImplementation testDependencies.junit
34+
testImplementation testDependencies.mockWebServer
3435
androidTestImplementation instrumentationTestDependencies.junit
3536
androidTestImplementation instrumentationTestDependencies.espressoCore
37+
androidTestImplementation instrumentationTestDependencies.testRules
3638
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
*
3+
* Copyright (c) Microsoft Corporation. All rights reserved.
4+
* Licensed under the MIT License.
5+
*
6+
*/
7+
8+
package com.microsoft.device.display.samples.widget
9+
10+
import androidx.preference.PreferenceManager
11+
import androidx.test.espresso.Espresso.onView
12+
import androidx.test.espresso.action.ViewActions.click
13+
import androidx.test.espresso.assertion.ViewAssertions.matches
14+
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
15+
import androidx.test.espresso.matcher.ViewMatchers.withText
16+
import androidx.test.filters.MediumTest
17+
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
18+
import androidx.test.platform.app.InstrumentationRegistry
19+
import androidx.test.rule.ActivityTestRule
20+
import com.microsoft.device.display.samples.widget.feed.RssFeed.DEFAULT_FEED_URL
21+
import com.microsoft.device.display.samples.widget.settings.SettingsActivity
22+
import org.junit.Assert.assertEquals
23+
import org.junit.Before
24+
import org.junit.Rule
25+
import org.junit.Test
26+
import org.junit.runner.RunWith
27+
28+
@RunWith(AndroidJUnit4ClassRunner::class)
29+
@MediumTest
30+
class SettingsUrlTest {
31+
32+
@get:Rule
33+
val activityRule = ActivityTestRule<SettingsActivity>(SettingsActivity::class.java)
34+
35+
private val appContext = InstrumentationRegistry.getInstrumentation().targetContext
36+
37+
@Before
38+
fun clearPreferences() {
39+
PreferenceManager.getDefaultSharedPreferences(appContext).edit().clear().commit()
40+
}
41+
42+
@Test
43+
fun shouldBeDefaultFeedUrl_whenGettingRequestUrlFromPreferences() {
44+
assertEquals(
45+
"Preferences Feed Url should be Default Feed Url",
46+
DEFAULT_FEED_URL,
47+
getUrlFromPreferences()
48+
)
49+
}
50+
51+
@Test
52+
fun shouldSavePreference_whenAnotherFeedUrlIsChosen() {
53+
onView(withText(R.string.widget_settings_predefined_title)).check(matches(isDisplayed()))
54+
onView(withText(R.string.widget_settings_custom_title)).check(matches(isDisplayed()))
55+
56+
onView(withText(R.string.widget_settings_predefined_title)).perform(click())
57+
58+
val secondKey = appContext.resources.getStringArray(R.array.appwidget_feeds)[1]
59+
val secondValue = appContext.resources.getStringArray(R.array.appwidget_feeds_value)[1]
60+
onView(withText(secondKey)).perform(click())
61+
62+
assertEquals(
63+
"Preferences Feed Url should be second value url",
64+
secondValue,
65+
getUrlFromPreferences()
66+
)
67+
}
68+
69+
private fun getUrlFromPreferences(): String? {
70+
val preferences = PreferenceManager.getDefaultSharedPreferences(appContext)
71+
return preferences.getString(
72+
appContext.resources.getString(R.string.widget_settings_predefined_key),
73+
DEFAULT_FEED_URL
74+
)
75+
}
76+
}
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/*
2+
*
3+
* Copyright (c) Microsoft Corporation. All rights reserved.
4+
* Licensed under the MIT License.
5+
*
6+
*/
7+
8+
package com.microsoft.device.display.samples.widget
9+
10+
import androidx.test.filters.MediumTest
11+
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
12+
import com.microsoft.device.display.samples.widget.feed.RssFeed
13+
import com.microsoft.device.display.samples.widget.feed.RssFeed.CHANNEL
14+
import com.microsoft.device.display.samples.widget.feed.RssFeed.CREATOR
15+
import com.microsoft.device.display.samples.widget.feed.RssFeed.DESCRIPTION
16+
import com.microsoft.device.display.samples.widget.feed.RssFeed.ITEM
17+
import com.microsoft.device.display.samples.widget.feed.RssFeed.LINK
18+
import com.microsoft.device.display.samples.widget.feed.RssFeed.PUB_DATE
19+
import com.microsoft.device.display.samples.widget.feed.RssFeed.TITLE
20+
import com.microsoft.device.display.samples.widget.feed.RssItem
21+
import org.hamcrest.MatcherAssert.assertThat
22+
import org.junit.Assert.assertEquals
23+
import org.junit.Assert.assertNotNull
24+
import org.junit.Test
25+
import org.junit.runner.RunWith
26+
import org.hamcrest.Matchers.`is` as iz
27+
28+
@RunWith(AndroidJUnit4ClassRunner::class)
29+
@MediumTest
30+
class XmlParserRssFeedTest {
31+
32+
@Test
33+
fun parseXml_shouldReturnEmptyList_whenNoRssItemsFound() {
34+
val xmlString =
35+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
36+
"<rss version=\"2.0\">" +
37+
" <$CHANNEL></$CHANNEL>" +
38+
"</rss>"
39+
40+
val rssItems = RssFeed.parseXml(xmlString)
41+
assertThat(
42+
"Method should return empty list if item tag is not found in xml",
43+
rssItems,
44+
iz(emptyList())
45+
)
46+
}
47+
48+
@Test
49+
fun parseXml_shouldReturnOneElementList_whenOneRssItemFound() {
50+
val xmlString =
51+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
52+
"<rss version=\"2.0\">" +
53+
" <$CHANNEL>\n" +
54+
" <$ITEM></$ITEM>\n" +
55+
" </$CHANNEL>" +
56+
"</rss>"
57+
58+
val emptyRssItem = RssItem(null, null, null, null, null)
59+
val rssItems = RssFeed.parseXml(xmlString)
60+
61+
assertEquals(
62+
"Method should return list containing one element if one item tag is found",
63+
1,
64+
rssItems.size
65+
)
66+
67+
assertEquals(
68+
"Rss Item should be empty when only item tag is encountered",
69+
emptyRssItem,
70+
rssItems[0]
71+
)
72+
}
73+
74+
@Test
75+
fun parseXml_shouldReturnEmptyList_whenItemTagIsLonger() {
76+
val xmlString =
77+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
78+
"<rss version=\"2.0\">" +
79+
" <$CHANNEL>\n" +
80+
" <$ITEM$ITEM></$ITEM$ITEM>\n" +
81+
" </$CHANNEL>" +
82+
"</rss>"
83+
84+
val rssItems = RssFeed.parseXml(xmlString)
85+
assertThat(
86+
"Method should return empty list if item tag name in xml is longer",
87+
rssItems,
88+
iz(emptyList())
89+
)
90+
}
91+
92+
@Test
93+
fun parseXml_shouldContainElementWithTitle_whenItemHasTitle() {
94+
val expectedTitle = "Build and test dual-screen web apps"
95+
val xmlString =
96+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
97+
"<rss version=\"2.0\">" +
98+
" <$CHANNEL>\n" +
99+
" <$ITEM>\n" +
100+
" <$TITLE>$expectedTitle</$TITLE>\n" +
101+
" </$ITEM>\n" +
102+
" </$CHANNEL>" +
103+
"</rss>"
104+
105+
val rssItem = RssFeed.parseXml(xmlString)[0]
106+
107+
assertNotNull(
108+
"Rss Item should not have null title",
109+
rssItem?.title
110+
)
111+
112+
assertEquals(
113+
"Rss Item should have title = $expectedTitle",
114+
expectedTitle,
115+
rssItem?.title
116+
)
117+
}
118+
119+
@Test
120+
fun parseXml_shouldContainElementWithCreator_whenItemHasCreator() {
121+
val expectedCreator = "Craig Dunn"
122+
val xmlString =
123+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
124+
"<rss version=\"2.0\">" +
125+
" <$CHANNEL>\n" +
126+
" <$ITEM>\n" +
127+
" <$CREATOR><![CDATA[$expectedCreator]]></$CREATOR>\n" +
128+
" </$ITEM>\n" +
129+
" </$CHANNEL>" +
130+
"</rss>"
131+
132+
val rssItem = RssFeed.parseXml(xmlString)[0]
133+
134+
assertNotNull(
135+
"Rss Item should not have null creator",
136+
rssItem?.creator
137+
)
138+
139+
assertEquals(
140+
"Rss Item should have creator = $expectedCreator",
141+
expectedCreator,
142+
rssItem?.creator
143+
)
144+
}
145+
146+
@Test
147+
fun parseXml_shouldContainElementWithDate_whenItemHasDate() {
148+
val expectedDate = "Thu, 03 Sep 2020 21:17:19"
149+
val actualDate = "Thu, 03 Sep 2020 21:17:19 +0000"
150+
val xmlString =
151+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
152+
"<rss version=\"2.0\">" +
153+
" <$CHANNEL>\n" +
154+
" <$ITEM>\n" +
155+
" <$PUB_DATE>$actualDate</$PUB_DATE>\n" +
156+
" </$ITEM>\n" +
157+
" </$CHANNEL>" +
158+
"</rss>"
159+
160+
val rssItem = RssFeed.parseXml(xmlString)[0]
161+
162+
assertNotNull(
163+
"Rss Item should not have null date",
164+
rssItem?.date
165+
)
166+
167+
assertEquals(
168+
"Rss Item should have date = $expectedDate",
169+
expectedDate,
170+
rssItem?.date
171+
)
172+
}
173+
174+
@Test
175+
fun parseXml_shouldContainElementWithLink_whenItemHasLink() {
176+
val expectedLink = "https://devblogs.microsoft.com/surface-duo/build-and-test-dual-screen-web-apps/"
177+
val xmlString =
178+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
179+
"<rss version=\"2.0\">" +
180+
" <$CHANNEL>\n" +
181+
" <$ITEM>\n" +
182+
" <$LINK>$expectedLink</$LINK>\n" +
183+
" </$ITEM>\n" +
184+
" </$CHANNEL>" +
185+
"</rss>"
186+
187+
val rssItem = RssFeed.parseXml(xmlString)[0]
188+
189+
assertNotNull(
190+
"Rss Item should not have null href",
191+
rssItem?.href
192+
)
193+
194+
assertEquals(
195+
"Rss Item should have href = $expectedLink",
196+
expectedLink,
197+
rssItem?.href
198+
)
199+
}
200+
201+
@Test
202+
fun parseXml_shouldContainElementWithDescription_whenItemHasShortDescription() {
203+
val actualDescription = "Hello dual-screen web developers! This is the day"
204+
val expectedDescription = "Hello dual-screen web developers! This is the day..."
205+
val xmlString =
206+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
207+
"<rss version=\"2.0\">" +
208+
" <$CHANNEL>\n" +
209+
" <$ITEM>\n" +
210+
" <$DESCRIPTION>$actualDescription</$DESCRIPTION>\n" +
211+
" </$ITEM>\n" +
212+
" </$CHANNEL>" +
213+
"</rss>"
214+
215+
val rssItem = RssFeed.parseXml(xmlString)[0]
216+
217+
assertNotNull(
218+
"Rss Item should not have null body",
219+
rssItem?.body
220+
)
221+
222+
assertEquals(
223+
"Rss Item should have body = $expectedDescription",
224+
expectedDescription,
225+
rssItem?.body
226+
)
227+
}
228+
229+
@Test
230+
fun parseXml_shouldContainElementWithDescription_whenItemHasLongDescription() {
231+
val builder = StringBuilder()
232+
for (index in 0 until RssFeed.DESCRIPTION_MAX_CHARS) {
233+
builder.append("a")
234+
}
235+
val expectedDescription = builder.append("...").toString()
236+
val actualDescription = expectedDescription + "description"
237+
238+
val xmlString =
239+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
240+
"<rss version=\"2.0\">" +
241+
" <$CHANNEL>\n" +
242+
" <$ITEM>\n" +
243+
" <$DESCRIPTION>$actualDescription</$DESCRIPTION>\n" +
244+
" </$ITEM>\n" +
245+
" </$CHANNEL>" +
246+
"</rss>"
247+
248+
val rssItem = RssFeed.parseXml(xmlString)[0]
249+
250+
assertNotNull(
251+
"Rss Item should not have null body",
252+
rssItem?.body
253+
)
254+
255+
assertEquals(
256+
"Rss Item should have body = $expectedDescription",
257+
expectedDescription,
258+
rssItem?.body
259+
)
260+
}
261+
}

Widget/app/src/main/java/com/microsoft/device/display/samples/widget/WidgetApp.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ class WidgetApp : AppWidgetProvider() {
7878

7979
override fun onReceive(context: Context, intent: Intent) {
8080
// Handling intent for list item click
81-
if (WidgetFactory.ACTION_INTENT_VIEW_TAG.equals(intent.action)) {
81+
if (WidgetFactory.ACTION_INTENT_VIEW_TAG == intent.action) {
8282
val url =
8383
intent.getStringExtra(WidgetFactory.ACTION_INTENT_VIEW_HREF_TAG)
8484
val webIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))

0 commit comments

Comments
 (0)