Skip to content

Commit

Permalink
[Widget] Added unit tests and instrumentation tests to the Widget Sam…
Browse files Browse the repository at this point in the history
…ple (#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
  • Loading branch information
bimiron authored Sep 21, 2020
1 parent d159eb3 commit 5be13fe
Show file tree
Hide file tree
Showing 12 changed files with 608 additions and 119 deletions.
2 changes: 2 additions & 0 deletions Widget/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ dependencies {
implementation networkDependencies.okhttpLoggingInterceptor

testImplementation testDependencies.junit
testImplementation testDependencies.mockWebServer
androidTestImplementation instrumentationTestDependencies.junit
androidTestImplementation instrumentationTestDependencies.espressoCore
androidTestImplementation instrumentationTestDependencies.testRules
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/

package com.microsoft.device.display.samples.widget

import androidx.preference.PreferenceManager
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.filters.MediumTest
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import com.microsoft.device.display.samples.widget.feed.RssFeed.DEFAULT_FEED_URL
import com.microsoft.device.display.samples.widget.settings.SettingsActivity
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4ClassRunner::class)
@MediumTest
class SettingsUrlTest {

@get:Rule
val activityRule = ActivityTestRule<SettingsActivity>(SettingsActivity::class.java)

private val appContext = InstrumentationRegistry.getInstrumentation().targetContext

@Before
fun clearPreferences() {
PreferenceManager.getDefaultSharedPreferences(appContext).edit().clear().commit()
}

@Test
fun shouldBeDefaultFeedUrl_whenGettingRequestUrlFromPreferences() {
assertEquals(
"Preferences Feed Url should be Default Feed Url",
DEFAULT_FEED_URL,
getUrlFromPreferences()
)
}

@Test
fun shouldSavePreference_whenAnotherFeedUrlIsChosen() {
onView(withText(R.string.widget_settings_predefined_title)).check(matches(isDisplayed()))
onView(withText(R.string.widget_settings_custom_title)).check(matches(isDisplayed()))

onView(withText(R.string.widget_settings_predefined_title)).perform(click())

val secondKey = appContext.resources.getStringArray(R.array.appwidget_feeds)[1]
val secondValue = appContext.resources.getStringArray(R.array.appwidget_feeds_value)[1]
onView(withText(secondKey)).perform(click())

assertEquals(
"Preferences Feed Url should be second value url",
secondValue,
getUrlFromPreferences()
)
}

private fun getUrlFromPreferences(): String? {
val preferences = PreferenceManager.getDefaultSharedPreferences(appContext)
return preferences.getString(
appContext.resources.getString(R.string.widget_settings_predefined_key),
DEFAULT_FEED_URL
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/

package com.microsoft.device.display.samples.widget

import androidx.test.filters.MediumTest
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import com.microsoft.device.display.samples.widget.feed.RssFeed
import com.microsoft.device.display.samples.widget.feed.RssFeed.CHANNEL
import com.microsoft.device.display.samples.widget.feed.RssFeed.CREATOR
import com.microsoft.device.display.samples.widget.feed.RssFeed.DESCRIPTION
import com.microsoft.device.display.samples.widget.feed.RssFeed.ITEM
import com.microsoft.device.display.samples.widget.feed.RssFeed.LINK
import com.microsoft.device.display.samples.widget.feed.RssFeed.PUB_DATE
import com.microsoft.device.display.samples.widget.feed.RssFeed.TITLE
import com.microsoft.device.display.samples.widget.feed.RssItem
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test
import org.junit.runner.RunWith
import org.hamcrest.Matchers.`is` as iz

@RunWith(AndroidJUnit4ClassRunner::class)
@MediumTest
class XmlParserRssFeedTest {

@Test
fun parseXml_shouldReturnEmptyList_whenNoRssItemsFound() {
val xmlString =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<rss version=\"2.0\">" +
" <$CHANNEL></$CHANNEL>" +
"</rss>"

val rssItems = RssFeed.parseXml(xmlString)
assertThat(
"Method should return empty list if item tag is not found in xml",
rssItems,
iz(emptyList())
)
}

@Test
fun parseXml_shouldReturnOneElementList_whenOneRssItemFound() {
val xmlString =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<rss version=\"2.0\">" +
" <$CHANNEL>\n" +
" <$ITEM></$ITEM>\n" +
" </$CHANNEL>" +
"</rss>"

val emptyRssItem = RssItem(null, null, null, null, null)
val rssItems = RssFeed.parseXml(xmlString)

assertEquals(
"Method should return list containing one element if one item tag is found",
1,
rssItems.size
)

assertEquals(
"Rss Item should be empty when only item tag is encountered",
emptyRssItem,
rssItems[0]
)
}

@Test
fun parseXml_shouldReturnEmptyList_whenItemTagIsLonger() {
val xmlString =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<rss version=\"2.0\">" +
" <$CHANNEL>\n" +
" <$ITEM$ITEM></$ITEM$ITEM>\n" +
" </$CHANNEL>" +
"</rss>"

val rssItems = RssFeed.parseXml(xmlString)
assertThat(
"Method should return empty list if item tag name in xml is longer",
rssItems,
iz(emptyList())
)
}

@Test
fun parseXml_shouldContainElementWithTitle_whenItemHasTitle() {
val expectedTitle = "Build and test dual-screen web apps"
val xmlString =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<rss version=\"2.0\">" +
" <$CHANNEL>\n" +
" <$ITEM>\n" +
" <$TITLE>$expectedTitle</$TITLE>\n" +
" </$ITEM>\n" +
" </$CHANNEL>" +
"</rss>"

val rssItem = RssFeed.parseXml(xmlString)[0]

assertNotNull(
"Rss Item should not have null title",
rssItem?.title
)

assertEquals(
"Rss Item should have title = $expectedTitle",
expectedTitle,
rssItem?.title
)
}

@Test
fun parseXml_shouldContainElementWithCreator_whenItemHasCreator() {
val expectedCreator = "Craig Dunn"
val xmlString =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<rss version=\"2.0\">" +
" <$CHANNEL>\n" +
" <$ITEM>\n" +
" <$CREATOR><![CDATA[$expectedCreator]]></$CREATOR>\n" +
" </$ITEM>\n" +
" </$CHANNEL>" +
"</rss>"

val rssItem = RssFeed.parseXml(xmlString)[0]

assertNotNull(
"Rss Item should not have null creator",
rssItem?.creator
)

assertEquals(
"Rss Item should have creator = $expectedCreator",
expectedCreator,
rssItem?.creator
)
}

@Test
fun parseXml_shouldContainElementWithDate_whenItemHasDate() {
val expectedDate = "Thu, 03 Sep 2020 21:17:19"
val actualDate = "Thu, 03 Sep 2020 21:17:19 +0000"
val xmlString =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<rss version=\"2.0\">" +
" <$CHANNEL>\n" +
" <$ITEM>\n" +
" <$PUB_DATE>$actualDate</$PUB_DATE>\n" +
" </$ITEM>\n" +
" </$CHANNEL>" +
"</rss>"

val rssItem = RssFeed.parseXml(xmlString)[0]

assertNotNull(
"Rss Item should not have null date",
rssItem?.date
)

assertEquals(
"Rss Item should have date = $expectedDate",
expectedDate,
rssItem?.date
)
}

@Test
fun parseXml_shouldContainElementWithLink_whenItemHasLink() {
val expectedLink = "https://devblogs.microsoft.com/surface-duo/build-and-test-dual-screen-web-apps/"
val xmlString =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<rss version=\"2.0\">" +
" <$CHANNEL>\n" +
" <$ITEM>\n" +
" <$LINK>$expectedLink</$LINK>\n" +
" </$ITEM>\n" +
" </$CHANNEL>" +
"</rss>"

val rssItem = RssFeed.parseXml(xmlString)[0]

assertNotNull(
"Rss Item should not have null href",
rssItem?.href
)

assertEquals(
"Rss Item should have href = $expectedLink",
expectedLink,
rssItem?.href
)
}

@Test
fun parseXml_shouldContainElementWithDescription_whenItemHasShortDescription() {
val actualDescription = "Hello dual-screen web developers! This is the day"
val expectedDescription = "Hello dual-screen web developers! This is the day..."
val xmlString =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<rss version=\"2.0\">" +
" <$CHANNEL>\n" +
" <$ITEM>\n" +
" <$DESCRIPTION>$actualDescription</$DESCRIPTION>\n" +
" </$ITEM>\n" +
" </$CHANNEL>" +
"</rss>"

val rssItem = RssFeed.parseXml(xmlString)[0]

assertNotNull(
"Rss Item should not have null body",
rssItem?.body
)

assertEquals(
"Rss Item should have body = $expectedDescription",
expectedDescription,
rssItem?.body
)
}

@Test
fun parseXml_shouldContainElementWithDescription_whenItemHasLongDescription() {
val builder = StringBuilder()
for (index in 0 until RssFeed.DESCRIPTION_MAX_CHARS) {
builder.append("a")
}
val expectedDescription = builder.append("...").toString()
val actualDescription = expectedDescription + "description"

val xmlString =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<rss version=\"2.0\">" +
" <$CHANNEL>\n" +
" <$ITEM>\n" +
" <$DESCRIPTION>$actualDescription</$DESCRIPTION>\n" +
" </$ITEM>\n" +
" </$CHANNEL>" +
"</rss>"

val rssItem = RssFeed.parseXml(xmlString)[0]

assertNotNull(
"Rss Item should not have null body",
rssItem?.body
)

assertEquals(
"Rss Item should have body = $expectedDescription",
expectedDescription,
rssItem?.body
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class WidgetApp : AppWidgetProvider() {

override fun onReceive(context: Context, intent: Intent) {
// Handling intent for list item click
if (WidgetFactory.ACTION_INTENT_VIEW_TAG.equals(intent.action)) {
if (WidgetFactory.ACTION_INTENT_VIEW_TAG == intent.action) {
val url =
intent.getStringExtra(WidgetFactory.ACTION_INTENT_VIEW_HREF_TAG)
val webIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
Expand Down
Loading

0 comments on commit 5be13fe

Please sign in to comment.