Wie teste ich eine Methode mit nem AsyncTask und Netzwerkverkehr?

DagobertDokate

DagobertDokate

Dauergast
137
Heute mal ganz allgemein:^^
Wie teste ich eine Methode mit nem AsyncTask?
JUnit4 scheint damit ja nicht ganz klar zu kommen, oder mir fehlt das nötige Wissen dazu..

Quasi so etwas:
Code:
 @Override
    public void refresh(final StartListItemTyp... itemTyps) {
        if (!onCreateWasCalled) {
            throw new IllegalStateException("onCreate() was not called");
        }
        if (itemTyps.length == 0) {
            throw new IllegalArgumentException("Es wurde keine Section übergeben.");
        }
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                AnnetLog.v(TAG, "refresh()");
                for (StartListItemTyp item : itemTyps) {
                    sendStartRefreshing(item);
                }
                try {
                    final List<StartEntry> newStartEntries = downloadHelper.downloadStartList();
                    final StartEntryComparator comparator = new StartEntryComparator();

                    for (StartEntry entry : newStartEntries) {
                        boolean contained = false;
                        for (StartListItemTyp itemTyp : itemTyps) {
                            if (itemTyp == entry.getTyp()) {
                                contained = true;
                                break;
                            }
                        }
                        if (contained) {
                            final StartEntry oldStartEntry = storageHelper.getStartListEntry(entry);
                            if (comparator.compare(oldStartEntry, entry) > 0) {
                                switch (entry.getTyp()) {
                                    case XXX:
                                        downloadNewsSection(entry);
                                    default:
                                        sendUpdated(entry.getTyp());
                                }
                            } else {
                                AnnetLog.v(TAG, "nothing to do with " + entry.getTyp());
                                sendUpdated(entry.getTyp());
                            }
                        }
                    }
                } catch (BException e) {
                    //FIXME Fehler weiterreichen
                }
                return null;
            }
        }.execute();
    }

lg. Dagobert
 
Schonmal ne gute Sache, dass du Unit Tests schreibst :thumbup:

Das Hauptproblem sehe ich darin, dass du den AsyncTask testen willst, aber das sollst du an dieser Stelle eigentlich nicht. Der AsyncTask ist eine eigene Klasse und in dem Sinne meiner Meinung nach auch eine eigene "Unit", die separat getestet wird.

Beim Unit-Testing will man ja (wie der Name schon sagt) einzelne Units ohne Abhängigkeiten zu anderen Units testen. Ich würde also erstmal den AsyncTask auslagern und separat testen. Wie man einen AsyncTask testbar macht und sauber testet, erklärt dieser Artikel sehr gut. Eine andere Möglichkeit zeigt dieser Artikel.

Bei deiner Methode solltest du dann nur noch 3 Dinge prüfen müssen:
  1. Wird onCreateWasCalled richtig behandelt
  2. Wird itemTyps.length richtig behandelt
  3. Wird der AsyncTask ausgeführt

Der spannende Teil ist natürlich Punkt 3. Wie testet man, ob eine Abhängigkeit aufgerufen wurde, obwohl man ja Abhängigkeiten in Unit Tests vermeiden will? Normalerweise nutzt man dafür z.B. ein Mocking Framework. Man mockt die Abhängigkeiten und die Mocks geben dem Tester die Möglichkeit, gewisse Dinge zu validieren. Zum Beispiel: Wurde Methode xy aufgerufen? / Wurde Methode xy mit Parameter z aufgerufen? / etc.

Damit kannst du die Units getrennt halten, aber trotzdem sicherstellen, dass die Interaktion zwischen ihnen funktioniert. In deinem Beispiel wäre dein AsyncTask also separat getestet und dein Test für die refresh-Methode stellt sicher, dass der AsyncTask auch ausgeführt wird (ohne, dass der AsyncTask im Test tatsächlich ausgeführt werden muss).

Das ganze funktioniert natürlich nur dann wirklich gut, wenn man testbaren Code (Google Testing Blog: Writing Testable Code) schreibt. Testbaren Code schreibt man aber wiederum nur, wenn man weiss, wie man testbaren Code schreibt :winki: Deshalb ist es ratsam, sich ein bisschen ins Thema einzulesen und dann seinen Code zu restrukturieren, damit er testbar(er) wird.

Ein paar Links zum Thema:
 
Zuletzt bearbeitet:
  • Danke
Reaktionen: Jaiel
Hallo Zoopa, danke für deine mühe =)
Aber bevor ich Anfange es umzumodel werde ich wohl nochmal weiter ausholen müssen.

Der Codeschnipsel ist aus meinem RefreshHelper.
Dieser soll dafür sorge tragen das ich mehrere Refreshcalls parallel abarbeiten kann.
Also ich habe eine Ansicht A... die kann per pull-to-refresh aktualisiert werden, ich habe eine Ansicht B... diese kann zeitgleich auch mit pull-to-refresh aktualisiert werden.
Beim starten der app sollen einmal alle bereiche erneuert werden.

Wie läuft ein Refresh genau ab?!
Die App verbindet sich mit einer vorgegebenen StartListe... dort stehen weitere links drin (sections) und Meta-Daten zu jeder URL wann sie das letzte mal geändert wurde (Datum & MD5). Die einzelne Section muss nur dann erneuert werden, wenn sich MD5 & Datum unterscheiden (ganz grob gesagt). Ansonsten wird einfach nichts getan. Falls was getan werden muss, wird der Link aus der XML-Datei gezogen, und auch dieser heruntergeladen und verarbeitet. War das ganze erfolgreich wird das neue Datum für die Section gespeichert, so das sie beim nächsten mal evt. nicht mehr herunter geladen werden muss.

Da das ganze dank Nebenläufigkeit nicht so easy ist, wie sich das in der Theorie anhört (hat da jemand nen Tipp für mich? =)), traue ich meinem RefreshHelper imo gar nicht über den Weg^^. Deswegen auch die Tests.

Jetzt stellt sich für mich die Frage, warum mein RefreshHelper keine Unit darstellt? Es ist doch eine abgeschlossene Unit? (Er hat Abhängigkeiten... aber deswegen ist es doch noch kein Integration-Test oder? Vor allem da ich ja whitebox testen will ?)

Die Methode refresh(...) ist zum aktualisieren da. Dabei muss beachtet werden ob nicht schon einmal das gleiche Element gerade aktualisiert wird. Wenn dies der Fall ist, muss dies ja nicht nochmals angestoßen werden. (Das ist grob die Aufgabe die mein RefreshHelper erledigen soll, dabei soll er halt noch die GUI Informieren ob gerade ein Bereich aktualisiert wird, oder wenn einer fertig ist, oder wenn ein bestimmter Bereich aktualisiert werden soll.

Jetzt zu meinem Problem was ich habe.
Benutzte ich ganz "normale" JUnit4-Test, kann ich wunderbar mit Mockito mein DownloadHelper, StorageHelper usw. mocken, jedoch bekomme ich Probleme beim sendUpdate()/sendError() da diese beide auf den lokalen BroadcastReceiver zurückgreifen, denn ich iwie leider nicht gemockt bekomme. oO
Dabei bin ich mir nicht mal sicher ob es daran liegt und ob der AsyncTask überhaupt ausgeführt wird unter JUnit, da dieser ja eigentlich die Android API benötigt oder?

Code:
        AnnetLog.v(TAG, "sendUpdated() " + itemTyp.name());
        currentRefreshing.remove(itemTyp);
        final Intent intent = new Intent(UPDATE_SUCCESS_EVENT);
        intent.putExtra("section", itemTyp.name());
        LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);

Dann habe ich mir überlegt Android-Testcases zu schreiben... also Test die wirklich unter Android ausgeführt werden. Jedoch bekomme ich da kein Mocking-Framework ans laufen (Mockito vorzugsweise), so das ich alle Mocks selbst schreiben müsste und dafür auch noch (mangels Injektion) den Code umbasteln müsste. So, dass das Gerät bei den Test nicht wirklich die komplette Methode durchlaufen würde, und auch den download nicht versuchen würde, da ich ja eigentlich gar nicht möchte.

Hier mal die reinen Unit Test-Cases
Code:
@RunWith(MockitoJUnitRunner.class)
@SmallTest
public class RefreshHelperTest {

    @Mock
    private StorageHelper storageHelper;

    @Mock
    private DownloadHelper downloadHelper;

    @Mock
    private Context context;

    @InjectMocks
    private RefreshHelperImpl helper;

    @Before
    public void setUp() throws Exception {
        helper.onCreate(context, storageHelper);
    }

    @Test(expected = IllegalStateException.class)
    public void testDoInitLoadNotCalledOnCreate() {
        final RefreshHelper refreshHelper = new RefreshHelperImpl();
        refreshHelper.doInitLoad();
    }

    @Test(expected = IllegalStateException.class)
    public void testRefreshNotCalledOnCreate() {
        final RefreshHelper refreshHelper = new RefreshHelperImpl();
        refreshHelper.refresh();
    }

    @Test(expected = IllegalStateException.class)
    public void testIsRefreshingNotCalledOnCreate() {
        final RefreshHelper refreshHelper = new RefreshHelperImpl();
        refreshHelper.isRefreshing(null);
    }

    @Test
    public void testDoCreate() {
        final RefreshHelperImpl refreshHelper = new RefreshHelperImpl();
        refreshHelper.onCreate(context, storageHelper);
    }

    @Test
    public void testIsRefreshingNotRefresh() {
        boolean isRefreshing = helper.isRefreshing(StartListItemTyp.aktuelles_table);
        assertFalse(isRefreshing);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testRefreshEmptyArguments() {
        helper.refresh();
    }

    @Test(expected = IllegalStateException.class)
    public void initLoadNotCalledOnCreate() {
        final RefreshHelperImpl refreshHelper = new RefreshHelperImpl();
        refreshHelper.doInitLoad();
    }

    @Test
    public void initLoad() {
        helper.doInitLoad();
    }
}
Wie bekomme ich es mit Junit hin, das die "normalen" Android Log-Einträge auch irgendwo ausgegeben werden?

Wie würde ein passender AndroidTestCase aussehen? oO

lg. Dagobert
 
Zuletzt bearbeitet:
Du könntest es auch mit einer Kombination aus Robolectric und Mockito versuchen.
Robolectric simuliert in normalen Unit Tests das Android Framework und gibt dir Möglichkeiten dich da reinzuhängen.
 
Jetzt stellt sich für mich die Frage, warum mein RefreshHelper keine Unit darstellt? Es ist doch eine abgeschlossene Unit? (Er hat Abhängigkeiten... aber deswegen ist es doch noch kein Integration-Test oder? Vor allem da ich ja whitebox testen will ?)

Deinen RefreshHelper würde ich schon als Unit ansehen. Für mich sind Units einzelne Klassen. Deshalb sehe ich den AsyncTask als eigene Unit, auch wenn du sie in deinem Fall als anynome Klasse implementiert hast. Ob meine Meinung korrekt ist, kann ich natürlich nicht sagen :winki:

Aber dadurch, dass ich den AsyncTask als eigene Unit sehe, würde ich ihn auch auslagern und separat testen. Klar, du braucht etwas Refactoring. Aber du sagst ja selbst, du traust deiner Klasse nicht so recht, also ist das ein guter Zeitpunkt dafür :thumbup:

Da das ganze dank Nebenläufigkeit nicht so easy ist, wie sich das in der Theorie anhört (hat da jemand nen Tipp für mich? =)), traue ich meinem RefreshHelper imo gar nicht über den Weg^^. Deswegen auch die Tests.
Ja asynchrone Dinge testen ist nicht immer ganz so leicht. Etwas, was dir bewusst sein muss, ist folgendes:
  1. Dein Test ruft die refresh-Methode auf
  2. Weil der AsyncTask asynchron ist, läuft die refresh-Methode weiter
  3. Die refresh-Methode ist sofort fertig, nachdem der AsyncTask beginnt
  4. Nachdem die refresh-Methode fertig ist, ist auch dein Test fertig. Egal, was mit deinem AsyncTask gerade läuft. Der Test selber läuft synchron, er wartet nur darauf, dass die refresh-Methode zurückkehrt.

Es ist also wichtig, dass der Test wartet, bis der AsyncTask fertig ist. Selbst wenn du alle Mocks richtig hinbekommst, musst der Test trotzdem auf den asynchronen Task warten, den du ja schliesslich testen willst. Bei deinem Beispiel sehe ich aber keinen Weg, wie du das erreichst, ohne den Code umzustrukturieren. Oder hast du eine Idee, die ich übersehen habe?

Ich mache das normalerweise so, dass der AsnycTask den Caller informiert, wenn er fertig ist. Das geht ganz simpel mit Hilfe eines Interfaces: siehe dieses Beispiel.

Mit diesem simplen Callback kannst du dann auch leichter auf diese Art den AsyncTask testen (Mittels CountDownLatch etc)
 
Zuletzt bearbeitet:
So ich habe mir heute morgen mal vor den Kopf gehauen... :lol:
und angefangen aufzuräumen.
So sieht mein RefreshHelper aktuell aus:
Auf die Queue hätte ich mal früher kommen sollen^^
Code:
public class RefreshHelperImpl implements RefreshHelper {
    private static final String TAG = StringUtils.substring(RefreshHelperImpl.class.getSimpleName(), 0, 22);

    public static final String UPDATE_ERROR_EVENT = "update-error-event";
    public static final String UPDATE_SUCCESS_EVENT = "update-success-event";
    public static final String UPDATE_START = "update-start-event";

    private Context mContext;
    private StorageHelper mStorageHelper;
    private DownloadHelper mDownloadHelper;
    private boolean mOnCreateWasCalled;
    private Queue<StartListItemType> mQueue;
    private DownloadQueueAsyncTask mDownloadTask;

    private final DownloadQueueAsyncTask.Callback mDownloadCallback = new DownloadQueueAsyncTask.Callback() {
        @Override
        public void updated() {
            sendUpdated();
        }

        @Override
        public void error(int resId) {
            sendError(resId);
        }

        @Override
        public void fatalError(int resId) {
            while(!mQueue.isEmpty()) {
                sendError(resId);
            }
        }
    };

    public RefreshHelperImpl() {
        mOnCreateWasCalled = false;
    }

    public void onCreate(final Context context, final StorageHelper storageHelper) {
        if (context == null) {
            throw new IllegalArgumentException("context can't be null");
        }
        if (storageHelper == null) {
            throw new IllegalArgumentException("storageHelper can't be null");
        }
        this.mContext = context;
        this.mStorageHelper = storageHelper;
        this.mDownloadHelper = new DownloadHelperImpl();
        this.mDownloadHelper.onCreate(context);
        mQueue = new ConcurrentLinkedQueue<>();
        mDownloadTask = new DownloadQueueAsyncTask(mDownloadCallback, mQueue, mStorageHelper, mDownloadHelper, mContext);
        this.mOnCreateWasCalled = true;
    }

    @Override
    public void doInitLoad() {
        Log.v(TAG, "doInitLoad()");
        refresh(StartListItemType.values());
    }

    @Override
    public void refresh(final StartListItemType... itemTyps) {
        Log.v(TAG, "refresh()");
        if (!mOnCreateWasCalled) {
            throw new IllegalStateException("onCreate() was not called");
        }

        if (itemTyps.length == 0) {
            throw new IllegalArgumentException("Es wurde keine Section übergeben.");
        }

        if(!mStorageHelper.existCssFile()){
            mQueue.offer(StartListItemType.Android_Smartphone_css);
        }

        for (StartListItemType type : itemTyps) {
            mQueue.offer(type);
        }
        if (!mDownloadTask.isRunning()) {
            mDownloadTask.start();
        }
    }

    @Override
    public boolean isRefreshing(final StartListItemType itemType) {
        if (!mOnCreateWasCalled) {
            throw new IllegalStateException("onCreate was not called");
        }
        final boolean isRefreshing = mQueue.contains(itemType);
        return isRefreshing;
    }

    private void sendUpdated() {
        StartListItemType type = mQueue.poll();
        Log.v(TAG, "sendUpdated() " + type.name());
        final Intent intent = new Intent(UPDATE_SUCCESS_EVENT);
        intent.putExtra("section", type.name());
        LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);
    }

    private void sendError(final int resId) {
        StartListItemType type = mQueue.poll();
        Log.v(TAG, "sendError() " + type.name());
        final Intent intent = new Intent(UPDATE_ERROR_EVENT);
        intent.putExtra("section", type.name());
        intent.putExtra("errorResId", resId);
        LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);
    }

    private void sendStartRefreshing(final StartListItemType itemTyp) {
        Log.v(TAG, "sendStartRefreshing() " + itemTyp);
        final Intent intent = new Intent(UPDATE_START);
        intent.putExtra("section", itemTyp.name());
        LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);
    }
}

Wie bekomme ich nun aber meine Callbacks aufgerufen?

lg. Dagobert
 

Ähnliche Themen

L
Antworten
15
Aufrufe
909
jogimuc
J
H
Antworten
2
Aufrufe
1.311
Hcman
H
M
Antworten
3
Aufrufe
168
moin
M
Zurück
Oben Unten