Arvutiteaduse instituut
  1. Kursused
  2. 2020/21 sügis
  3. Mobiiliarvutus ja asjade internet (LTAT.06.009)
EN
Logi sisse

Mobiiliarvutus ja asjade internet 2020/21 sügis

  • Main
  • Lectures
  • Labs
  • Homeworks & Home Assignments
  • Quizes
  • Task submission
  • Extra Materials
  • Projects
    • Teams & Topics
    • Presentations & Report
    • Grading & Submission

Lab 7 - Testing a Shopping Basket App

Our starting point is an existing application for managing a shopping basket. Download Android Studio project here

Key classes we will be testing, their expected behaviour:

ShoppingItem

  • Has properties: name, price, quantity.
  • Price is equal to string length of item name.
  • has method getReformattedName(), which transforms names consisting only of numbers ( such as product code) by prefixing them with "Product #", for example, the name "12345" becomes "Product #12345"

ShoppingBasket

a collection of ShoppingItems Provides methods:

  • addItem(item:ShoppingItem) -- When adding a ShoppingItem with a name already contained in the basket, the previously existing ShoppingItems' quantity gets updated, instead of adding a new element to the list
  • getTotalPrice(): Int -- should return the total cost of all items in the basket (taking into account quantity)
  • noOfUniqueProducts(): Int --- How many uniquely named products are in the basket

BasketStorageHelper -

Helper to save/load ShoppingBasket objects to/from SharedPreferences.

  • saveBasket(basket: ShoppingBasket, sharedPrefs: SharedPreferences) { ... }
    • Before saving to SharedPreferences, transforms the ShoppingBasket into a json String representation.
  • loadBasket(sharedPrefs: SharedPreferences) { ... }
    • Parses the stored json representation into a ShoppingBasket. If no previous value exists in SharedPreferences, returns a new, empty ShoppingBasket

BasketStorageHelper is used to store the basket state to disk when app is closed (and reload it when re-opened)


Local Unit Tests

  • Write local unit tests for the cases defined in ShoppingTests.kt
  • Run the tests with code coverage, try to get 100% line coverage for ShoppingBasket class

Note: with a local unit test for ShoppingItems getReformattedName() method, you will get exceptions. This is expected.

  • ShoppingBasket has two bugs with respect to above defined app behaviour, see if you can find through a failing test case.

Roboelectric

Add Robolectric to your project. Roboelectric provides standard JVM compliant implementations of the Android framework APIs for you so you can use them in your local unit tests. in build.gradle:

  • Inside android { .. } add:
    testOptions {
        unitTests {
            includeAndroidResources = true
        }
    }
  • In dependencies { .. } add:
  androidTestImplementation 'androidx.test.ext:junit:1.1.2'
  testImplementation 'androidx.test:core:1.3.0'
  testImplementation "org.robolectric:robolectric:4.3.1"

Create a new test class, annotate with @RunWith(RobolectricTestRunner::class), as below:

    @RunWith(RobolectricTestRunner::class)
    class RoboelectricUnitTest {
        ...
    }
  • With Roboelectric, you can test the getReformattedName() method mentioned previously.
  • If you get errors related to Java 9 and SDK levels, also add
   @Config(sdk=[Build.VERSION_CODES.P])

Add more tests so that you have 100% coverage of lines for both ShoppingItem and ShoppingBasket

Instrumented Unit Tests

Complete the test cases defined in ExampleInstrumentedTest.kt (androidTest folder)

The idea is to validate the integration between BasketStorageHelper and SharedPreferences API.

  • You can get a SharedPreferences object similar to how it is already done in MainActivity: sharedPrefs = getSharedPreferences(MainActivity.APP_PREFS, MODE_PRIVATE)
  1. If a certain .json is already contained in SharedPreferences, does BasketStorageHelper read it in correctly?
  2. After calling BasketStorageHelper saveBasket(), is the data written into SharedPreferences in the expected format ?

Note: With our examples, the same test case would also work as a Roboelectric test!

Espresso UI tests

Espresso Cheat sheet

Add Espresso dependencies to your project:

  androidTestImplementation 'androidx.test:rules:1.3.0'
  androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

Create a new class for your Espresso tests (under instrumented tests):

  @RunWith(AndroidJUnit4::class)
  class EspressoTests {
   }

Let's create a new test case which:

  • Checks that text "Milk" is not displayed/doesn't exist
  • Performs typing on view with id editText_name, entering text "Milk"
  • Clicks the view with text "Add"
  • Checks that view with text "Milk" is displayed (exists)

Note: by default, no Activity is open when running an UI test, so you should launch MainActivity explicitly with ActivityScenario.launch(MainActivity::class.java)

  • Try writing some UI tests for other cases, e.g. reading same name twice.

Note: If you are running multiple tests that are affecting the same SharedPreferences, the tests may start failing since one test is affecting the state of another. Instead, it's a good idea to clear the state before each test.

    private fun clearSharedPrefs() {
        val sharedPreferences = getInstrumentation().targetContext
            .getSharedPreferences(MainActivity.APP_PREFS, Context.MODE_PRIVATE)
        sharedPreferences.edit().clear().commit()
    }

The above code uses instrumentation to get a handle of SharedPreferences and clears it. You could use it at the beginning of each test case. But that introduces a lot of repetitive code. Instead, it's better to use rules to define such behaviour.

In this case, let's add a @Before rule, which clears the shared preferences and opens MainActivity for us.

    @Before
    fun cleanUp(){
        clearSharedPrefs()
        ActivityScenario.launch(MainActivity::class.java)
    }

Bonus: Test driven development

Test driven development is the practice of first designing and implementing the test cases and then implementing the necessary features to make the tests pass.

Let's add a feature where the basket price is given a 20% price discount if it contains 5 or more unique items. Additionally, in the UI, if the user has already added 4 items, a notification should show them that they are close to getting the discount.

The tests are implemented for you, try to update the app implementation to make the tests pass. We have one local unit test and one Espresso test.

Local unit test:

    @Test
    fun fiveOrMoreUniqueItemsGives20PercentDiscount(){
        // TODO: Make me pass (implement missing feature)

        val basket = ShoppingBasket()

        repeat(5){  basket.addItem(ShoppingItem("Item$it"))  }

        val fullPrice = basket[0].price * 5
        val expectedDiscountPrice = (fullPrice * 0.8).toInt() // -20% discount

        assertEquals(expectedDiscountPrice, basket.getTotal())
    }

Espresso test:

    @Test
    fun addingFourUniqueItemsShowsDiscountHint() {
        // TODO: Make me pass (implement missing feature)
        onView(withText("Add 1 more item to get 20% off!")).check(doesNotExist())
        //Add 4 items
        repeat(4){
            onView(withId(R.id.editText_name))
                .perform(typeText("Product$it"), closeSoftKeyboard())
            onView(withId(R.id.button)).perform(click())
        }
        onView(withText("Add 1 more item to get 20% off!")).check(matches(isDisplayed()))
    }
  • Arvutiteaduse instituut
  • Loodus- ja täppisteaduste valdkond
  • Tartu Ülikool
Tehniliste probleemide või küsimuste korral kirjuta:

Kursuse sisu ja korralduslike küsimustega pöörduge kursuse korraldajate poole.
Õppematerjalide varalised autoriõigused kuuluvad Tartu Ülikoolile. Õppematerjalide kasutamine on lubatud autoriõiguse seaduses ettenähtud teose vaba kasutamise eesmärkidel ja tingimustel. Õppematerjalide kasutamisel on kasutaja kohustatud viitama õppematerjalide autorile.
Õppematerjalide kasutamine muudel eesmärkidel on lubatud ainult Tartu Ülikooli eelneval kirjalikul nõusolekul.
Courses’i keskkonna kasutustingimused