Metody testowania aplikacji w systemie Android

Wstęp

Aplikacje mobilne, które są dostępne na dzisiejsze urządzenia nie przypominają już aplikacji znanych z dawnych telefonów komórkowych z przed ery smartfonów. Stają się one coraz bardziej złożone, oferują synchronizację w chmurze, stawiają na jak najlepszy user experience przy jednoczesnej optymalizacji pod względem wykorzystania procesora, pamięci oraz zasobów baterii.

Dlatego też jest to bardzo ważne, aby traktować takie aplikacje mobilne jak pełnoprawne aplikacje desktopowe oraz skupiać się na odpowiednim ich przetestowaniu.

Strategie testowania

Testy manualne

Testy automatyczne bardzo często nie są w stanie wychwycić wszystkich problemów jakie może posiadać rozwijana aplikacja (na przykład w testowaniu user experience), dlatego dużą role stanowią testy manualne na rzeczywistych urządzeniach.

Niestety w rzeczywistości nie sposób przetestować aplikacji na wszystkich urządzeniach, więc dlatego często przyjmuje się strategie testowania na urządzeniach które posiadają najniższą i najwyższą możliwą konfigurację (np. rozdzielczość lub wielkość ekranu, wersja Androida).

Dużym udogodnieniem mogą być również “wypożyczalnie” sprzętu do którego możemy się podłączać zdalnie, takie jak Nativetap.

Testy automatyczne

Oczywiście w przypadku nawet mało zaawansowanych projektów trudno o efektywne utrzymywanie poprawności implementacji z zastosowaniem jedynie testów manualnych. Testy automatyczne pozwalają nam nie tylko sprawdzać poprawność aktualnie zaimplementowanych funkcjonalności, ale również zapobiegać ich przypadkowych degradacji w przyszłości.

Testy automatyczne możemy podzielić na testy jednostkowe oraz integracyjne. W przypadku testów jednostkowych chcemy przetestować pojedyncze jednostki programu na przykład metod lub obiektów. W przypadku testów integracyjnych chcemy sprawdzać jak działają całe funkcjonalności aplikacji.

Dla przykładu przyjmijmy, że przycisk w pewnej aktywności jest używany do tego aby przejść do drugiej aktywności. Dla takiego przypadku test jednostkowy mógłby sprawdzać czy odpowiednia intencja została uruchomiona, natomiast test integracyjny powinien sprawdzać czy druga aktywność została poprawnie uruchomiona po przyciśnięciu przycisku.

Typy testów automatycznych w Androidzie

Testy automatyczne można podzielić, na te, które są uruchamiane na maszynie wirtualnej JVM (tak zwane testy lokalne) oraz na te które wymagają systemu Android (testy instrumentalne).

Do testów lokalnych należą testy, które nie wymagają frameworku Androida z czego wynika bezpośrednio zaleta tych testów – wpływa to na ich dużą szybkość wykonywania się w porównaniu do testów instrumentalnych. Dlatego też testy te są bardzo dobre do weryfikacji logiki małych jednostek kodu niezależnie od API Android.

W przypadku gdy testy wymagają użycia API Android należy wykorzystać testy instrumentalne, które mogą być wykonywane tylko w systemie Android. Skutkuje to ich dłuższym czasem wykonywania.

Dla obu opisanych powyżej typów testów najczęściej wykorzystywanym frameworkiem jest JUnit.

Testy JUnit

Ten rozdział został poświęcony na przedstawienie przykładowych testów lokalnych i instumentalnych z wykorzystaniem frameworku JUnit.

Projekt startowy do testowania (initial-project) jak i ostateczny projekt ze wszystkimi testami opisanymi poniżej (final-project) można znaleźć w repozytorium Github.

W projektach Java oraz Android przyjęto następującą konwencję organizacji plików źródłowych:

  • źródła Java aplikacji umieszcza się w ścieżce src/main/java,
  • źródła testów lokalnych Java w ścieżce src/test/java,
  • natomiast źródła testów wymagających środowisko Android w ścieżce src/androidTest/java.

Zaleca się, aby przed rozpoczęciem analizowania poniższych przykładów zaimportować projekt startowy z katalogu initial-project z wcześniej podanego repozytorium.

Test lokalny JUnit – przykład

Konfiguracja

Przed rozpoczęciem implementacji testów JUnit, należy najpierw dodać framework do systemu zarządzania zależnościami (standardowo Gradle w przypadku Android). W przypadku Gradle należy dodać bibliotekę JUnit do elementu dependencies w pliku build.gradle:

dependencies {
    ...
    testCompile 'junit:junit:4.12'
}

Następnie należy również stworzyć nowy katalog przeznaczony dla testów lokalnych. W przypadku środowiska Android Studio najlepiej zrobić to klikając prawym przyciskiem na katalog app w widoku projektu, a następnie wybrać opcję New/Folder/Java Folder.
Następnie w otwartym oknie należy zaznaczyć opcję Change Folder Location i jako New Folder Location wpisać src/test/java – czyli ścieżkę gdzie będziemy umieszczać testy lokalne.

01-test-folder-creating

Aby zobaczyć utworzoną strukturę można przełączyć się na widok Project z domyślnej opcji Android. Następnie w katalogu src/test/java należy utworzyć nowy test lokalny, który pozwoli nam na przetestowanie prostej klasy do konwersji walut CurrencyConverter. W tym celu należy najpierw stworzyć nową paczkę odpowiadającą tej dla klasy CurrencyConverter w katalogu src/test/java (w przypadku naszego projektu jest to com.panum.edu.testingtutorialandroid). Następnie należy w paczce utworzyć nowy test (tworząc nową klasę Java) o nazwie CurrencyConverterUnitTest.

Ostatecznie powinna być widoczna poniższa struktura projektu:

02-project-structure

W przypadku wystąpienia problemów związanych ze środowiskiem proszę o zapoznanie się z dodatkowymi informacjami z oficjalnej strony Android.

Implementacja testu

W katalogu src/test/java we wcześniej utworzonej klasie com.panum.edu.testingtutorialandroid.CurrencyConverterUnitTest należy umieścić implementację testów dla klasy CurrencyConverter:

package com.panum.edu.testingtutorialandroid;

import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;

public class CurrencyConverterUnitTest {
    private static final double DELTA = 0.00000000001;
    private CurrencyConverter currencyConverter;

    @Before
    public void setUp() {
        currencyConverter = new CurrencyConverter();
    }

    @Test
    public void shouldAddsNewCurrency() {
        currencyConverter.addCurrency("TEST", 1);
        assertArrayEquals("Adding new currency failed", new String[]{"TEST"},
                currencyConverter.getCurriencies());
    }

    @Test(expected = IllegalArgumentException.class)
    public void shouldThrowsExceptionWhileAddingCurrencyWithNullCode() {
        currencyConverter.addCurrency(null, 1);
    }

    @Test(expected = IllegalArgumentException.class)
    public void shouldThrowsExceptionWhileAddingCurrencyWithEmptyCode() {
        currencyConverter.addCurrency("", 1);
    }

    @Test(expected = IllegalArgumentException.class)
    public void shouldThrowsExceptionWhileAddingCurrencyWithNegativeRate() {
        currencyConverter.addCurrency("TEST", -1);
    }

    @Test
    public void shouldConvertsFromOneCurrencyToAnother() {
        currencyConverter.addCurrency("TEST1", 1);
        currencyConverter.addCurrency("TEST2", 2);
        double result = currencyConverter.convert(10, "TEST1", "TEST2");
        assertEquals("Converting currencies failed", 20, result, DELTA);
    }

    @Test
    public void shouldReturnsTheSameAmount() {
        currencyConverter.addCurrency("TEST", 2);
        double result = currencyConverter.convert(10, "TEST", "TEST");
        assertEquals("Converting currencies failed", 10, result, DELTA);
    }

    @Test
    public void shouldReturnsEmptyCurrenciesArray() {
        assertArrayEquals("Returning empty currencies failed",
                new String[]{}, currencyConverter.getCurriencies());
    }
}

Powyższa implementacja składa się z 7 testów (oznaczonych adnotacją Test) oraz dodatkowej metody inicjalizującej pole currencyConverter (adnotacją Before można oznaczyć metodę, która będzie miała się wywoływać przed każdym osobnym testem).

Wśród testów można zauważyć takie, które dotyczą bezpośrednich funkcjonalności klasy CurrencyConverter, ale też sprawdzających poprawność walidacji danych przez metody tej klasy.

W przypadku testów funkcjonalności najczęściej używane są metody takie jak assertEquals lub assertArrayEquals, które pozwalają na weryfikację poprawności zwracanych przez metody wartości.

Jeśli chodzi o testy weryfikujące walidację parametrów, to bardzo przydatną opcją jest zdefiniowanie spodziewanego wyjątku, który miałby być rzucony za pomocą parametru expected adnotacji Test.

Najprostszym sposobem na uruchomienie testu jest wybór opcji Run ‚CurrencyConverterUnitTest’ po kliknięciu na test prawym przyciskiem. Przed uruchomieniem należy również pamiętać aby opcję Test Artifacts ustawić na Unit Tests w widoku Build Variants:
03-unit-test-artifact
Jak można zauważyć w zakładce Run po uruchomieniu testu, wszystkie testy przechodzą poprawnie.

Test instrumentalny JUnit – przykład

Konfiguracja

Podobnie jak w przypadku testów lokalnych, należy najpierw dodać JUnit do elementu zależności dependencies.

dependencies {
    ...
    testCompile 'junit:junit:4.12'
}

Tak jak napisano wcześniej, zaleca się, aby testy instrumentalne przechowywać w ścieżce src/androidTest/java. W tym celu należy ponownie utworzyć Java Folder oraz paczkę podobnie jak przedstawiono w przykładzie testów lokalnych.

Tak jak powiedziano wcześniej, testy instrumentalne wymagają uruchomienia na maszynie Android, dzięki czemu dostępna jest możliwość przetestowania kodu aplikacji opartego na API Android. Również dodatkowo API Android do testowania umożliwia możliwość zarządzania cyklem życia aplikacji oraz generowania wydarzeń interakcji użytkownika.

Ostatecznie należy stworzyć nową klasę CurrencyConverterActivityFuncTest w utworzonym pakiecie w ścieżce src/androidTest/java. Po powyżej opisanej konfiguracji, struktura plików dla testów instrumentalnych powinna wyglądać jak poniżej:
04-project-structure

Implementacja testu

We wcześniej utworzonym katalogu src/androidTest/java we wcześniej utworzonej klasie com.panum.edu.testingtutorialandroid.CurrencyConverterActivityFuncTest należy umieścić implementację testów dla klasy CurrencyConverterActivity – naszym celem tym razem będzie przetestowanie nie tylko samego konwertera, lecz całej funkcjonalności (aktywności).

package com.panum.edu.testingtutorialandroid;

import android.content.Intent;
import android.test.ActivityUnitTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;

public class CurrencyConverterActivityFuncTest
        extends ActivityUnitTestCase<CurrencyConverterActivity> {

    private static final double DELTA = 0.00000000001;

    private CurrencyConverterActivity activity;

    private Spinner fromSpinner;
    private Spinner toSpinner;
    private EditText amountEditText;
    private EditText resultEditText;
    private Button convertButton;

    public CurrencyConverterActivityFuncTest() {
        super(CurrencyConverterActivity.class);
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        Intent intent = new Intent(getInstrumentation().getTargetContext(),
                CurrencyConverterActivity.class);
        startActivity(intent, null, null);
        activity = getActivity();
        fromSpinner = (Spinner) activity.findViewById(R.id.fromSpinner);
        toSpinner = (Spinner) activity.findViewById(R.id.toSpinner);
        amountEditText = (EditText) activity.findViewById(R.id.amountEditText);
        resultEditText = (EditText) activity.findViewById(R.id.resultEditText);
        convertButton = (Button) activity.findViewById(R.id.convertButton);
    }

    @SmallTest
    public void testShouldConvertsFromOneCurrencyToAnother() {
        CurrencyConverter currencyConverter = new CurrencyConverter();
        currencyConverter.addCurrency("TEST1", 1);
        currencyConverter.addCurrency("TEST2", 2);
        activity.setCurrencyConverter(currencyConverter);

        activity.addItemsToSpinner(fromSpinner, currencyConverter.getCurriencies());
        ArrayAdapter fromSpinnerAdapter = (ArrayAdapter) fromSpinner.getAdapter();
        int fromSpinnerPosition = fromSpinnerAdapter.getPosition("TEST1");
        fromSpinner.setSelection(fromSpinnerPosition);
        assertEquals("Invalid from spinner value", "TEST1", fromSpinner.getSelectedItem().toString());

        activity.addItemsToSpinner(toSpinner, currencyConverter.getCurriencies());
        ArrayAdapter toSpinnerAdapter = (ArrayAdapter) toSpinner.getAdapter();
        int toSpinnerPosition = toSpinnerAdapter.getPosition("TEST2");
        toSpinner.setSelection(toSpinnerPosition);
        assertEquals("Invalid to spinner value", "TEST2", toSpinner.getSelectedItem().toString());

        amountEditText.setText("10");
        assertEquals("Invalid to amount edit value", "10", amountEditText.getText().toString());

        convertButton.performClick();

        double result = Double.parseDouble(resultEditText.getText().toString());
        assertEquals("Converting currencies failed", 20, result, DELTA);
    }
}

Jak widać, w tym przypadku występuje jeden test oraz – podobnie jak wcześniej – metoda, która jest wywoływana przed każdym testem. W metodzie tej zapisywane są wszystkie potrzebne komponenty do testów.

Jeśli chodzi o sam test, to w początkowej części kodu ustawiane są najpierw odpowiednie wartości na poszczególnych komponentach, żeby ostatecznie wywołane zostało kliknięcie na przycisk konwertowania za pomocą metody performClick. Ostatecznie sprawdzane jest czy wynikowe pole tekstowe zawiera odpowiednią wartość wynikową.

Tak jak w przypadku wcześniejszych testów, test można uruchomić za pomocą opcji Run po kliknięciu prawym przyciskiem myszy, jednak należy pamiętać o wcześniejszym wybraniu opcji Android Instrumentation Tests w widoku Build Variants:

05-build-variants-instrumentation

Podsumowanie

W powyższym artykule przedstawiono podstawowe techniki oraz narzędzia testowania aplikacji w systemie Android, które pomagają w podniesieniu jakości aplikacji oraz zmniejszają ilość zasobów potrzebnych na jej utrzymanie. Testy lokalne pozwalają na testowanie logiki małych części systemu, natomiast testy wymagające systemu Android testują integrację wytworzonych funkcjonalności, co pozwala na zadowalające pokrycie kodu  testami.

Jedna uwaga do wpisu “Metody testowania aplikacji w systemie Android

  1. Jakub, napisałeś świetny, bardzo kompleksowy artykuł. Przy testach manualnych warto pamiętać nie tylko o rozdzielczości i systemie, ale również np. pamięci RAM. W najnowszych urządzeniach, przy wymagających aplikacjach, warto zwrócić uwagę na współdziałanie CPU i GPU, oraz jak to wpływa na wydajność aplikacji. Dzięki za zauważenie potencjału Nativetap:)

    Polubienie

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Wyloguj /  Zmień )

Zdjęcie na Google

Komentujesz korzystając z konta Google. Wyloguj /  Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Wyloguj /  Zmień )

Zdjęcie na Facebooku

Komentujesz korzystając z konta Facebook. Wyloguj /  Zmień )

Połączenie z %s