VBM on Android - Retrofit Functional Tests

VBM on Android - Retrofit Functional Tests

The first thing I'll do for this post is define my use of Functional Tests. This is something I feel needs to be done because the related type of testing, Integration Tests can easily be swapped for each other. In fact I work with people who use them in opposite fashion.

I'm using Functional Test to mean tests which look at the function of code working together; It ensures they function together.
This is a step up/back/out from a Unit Test which ensures a small unit works as expected.
Functional Tests are still run in an isolated environment; no interaction with the outside world. No accessing real resources.

This leaves Integration Tests as the tests that ensure working with real versions of other systems. Make the network calls; use a database; hit the file system.

This is the definition I'm working with for this post (and probably more).


Where we last left our HackerNews Reader we had a reference architecture for our networking set up. This was done pretty well; a few bits done quick for implementation purposes.
I find it painful thought to see a couple classes with zero tests. The offenders are the ItemId and Items classes. It's not that they aren't TDD'd; we did TDD them into existence, just not into function. I'll play a little nice with myself... :\ and leave the classes in since the current existence is driven by requirements of other classes.

For example; our Items class; particularly the size

public class Items {
    private final ItemId[] itemIds;

    public Items(ItemId[] itemIds) {
        this.itemIds = itemIds;
    }

    public int size() {
        return itemIds.length;
    }
}

came into existence from our ItemNetworkTests.

public class ItemNetworkTests {
    @Test
    public void makeTopStoriesRequest() throws IOException {
        final Call<Items> topStoriesCall = new ItemNetwork(mockWebServer.url("/")).topStories();
        final Items items = topStoriesCall.execute().body();
        assertNotNull(items);
        assertEquals(2, items.size());
    }
}

What should have been done; is once we realized we needed the size method is to go write the unit tests around it and then came back and used it in ItemNetworkTests. We'll delete go back and do that now; via TDD; including changes to go Red first.

Here's why it's a huge value to do tests. SURE - it'll come up eventually, but!!! (This is also why I like Kotlin) NULLS!!!

public int size() {
    return itemIds.length;
}

My test is

    @Test
    public void size(){
        assertEquals(0, new Items(null).size());
    }

And BOOM! We've got some NPE.
Of course this was a simplistic implementation with no expectation of it being a good one - but it stands out as something that'd never have existed if I'd done the TDD properly.

While this is about setting up some Functional Tests around the Retrofit networking; this isn't a HOW-TO of TDD; I'll cover it, and utilize it; but I want to avoid a step by step of doing TDD on things. :)

Because I really like assertJ and want some Lambda's; I'm updating to use Java8 and the Jack compiler.

OK; did that; can cleanly get some exception testing in. Added a dfew tests around creation of Items. Added a test class for ItemId. All it does currently is equality.

Now we can get onto the Functional Tests! I think...

While we earlier left the networking with just a wrapper around the Retrofit builder in the ItemNetwork class; this only kinda works for doing a Functional Test. I look at this system as "What do I want the behavior to be?" and in this case it's getting back an object; particularly an Items object for the topStories call. I don't want a Call<> object; then I have to deal with that. Something should deal with that for me... I just gotta... build that.

I name this something simple and stupid; ItemAccess. I've seen it named ItemService; as Retrofit itself favors calling these "Service"s; and according to some architectural designs; it's the "Service" layer... But honestly; Don't use things that have meaning! Service is a thing in android; and these aren't services... /rant

Which leads me to call it something else; I'm doing Access; please let me know why your name is better.

The ItemAccess class will not be in the internal package; like the ItemNetwork class it. This is intended to be the access point to the rest of the application.

Small note for those paying any attention to my commit history; in AndroidStudio I pop open the terminal and do a 'quick commit' a lot. If this was a team a project; we all would be doing this; and I'd include git pull before/after each commit.

Part of wanting the data back is that I want it back asynchronously. Our earlier ItemNetworkTests tested synchronously, forcing us to do something new here. Part of why I opt for Retrofit is the simplicity in doing async. There is a Callback<> interface that is enqueued and will be called when the network request is complete.

The anonymous implementation looks like

new retrofit2.Callback<Items>(){
    @Override
    public void onResponse(Call<Items> call, Response<Items> response) {    }

    @Override
    public void onFailure(Call<Items> call, Throwable t) {    }
}

Setting up the check for a successful topStories request makes use of the equality implemented in the ItemId class.

            public void onResponse(Call<Items> call, Response<Items> response) {
                assertThat(response.isSuccessful()).isTrue();
                assertThat(response.body()).isNotNull();
                final Items items = response.body();
                assertThat(items.size()).isEqualTo(2);
                assertThat(items.contains(new ItemId(10000))).isTrue();
                assertThat(items.contains(new ItemId(2))).isTrue();
                latch.countDown();
            }

While we don't need to implement the item specific check; in TDD; we could just create any ol' values and still get the size to pass. Even if this is refactored away later, it's needed now to establish functionality.

Run Tests
Fails... Wha?
I love this part of programming... I've got

new ItemAccess().topStories(new Callback<Items>(){
            @Override
            public void onResponse(Call<Items> call, Response<Items> response) {
...

and ... well... I never implemented ItemAccess#topStories... Ooops... hehe

Not that the implementation is terribly hard - It's mostly to hide the async mechanism.

    public void topStories(Callback<Items> callback) {
        new ItemNetwork().topStories().enqueue(callback);
    }

The ItemNetwork class encapsulates building up the network connection; the ... I was gonna say the ItemAccess encapsulates the async mechanism. This doesn't feel right. I mean; it would; but it smells of violating the Law of Demeter.
It's also not doing what it says. ItemNetwork#topStories isn't returning top stories; it's returning a Call and then things happen... Nope. Don't like this at all.
...
I'm going back and forth a bit. The ItemNetwork understands how to construct the Call<>. Not how to start the request; just builds it. ItemAccess understands how to start/trigger (avoiding 'execute') the network request. Any flags or execute vs enqueue method calls are made in this.

That feels better - the understands approach can help. I assume I'm doing it wrong; but it's still helping keep each class to a single responsibility.

Here's the first Function Test of the network layer.

public class ItemAccessTests {

    @Rule
    public final MockWebServer mockWebServer = new MockWebServer();

    @Test
    public void topStories() throws Exception {
        mockWebServer.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody("[10000,2]"));

        new ItemNetwork(mockWebServer.url("/"));//For The Tests!

        final CountDownLatch latch = new CountDownLatch(1);
        new ItemAccess().topStories(new Callback<Items>(){
            @Override
            public void onResponse(Call<Items> call, Response<Items> response) {
                assertThat(response.isSuccessful()).isTrue();
                assertThat(response.body()).isNotNull();
                final Items items = response.body();
                assertThat(items.size()).isEqualTo(2);
                assertThat(items.contains(new ItemId(10000))).isTrue();
                assertThat(items.contains(new ItemId(2))).isTrue();
                latch.countDown();
            }

            @Override
            public void onFailure(Call<Items> call, Throwable t) {

            }
        });

        assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue();
    }
}

This uses multiple classes and goes through them all. I'm finding if I can write each level up as a functional test of all the lower levels; simplifies mocking/fakes/etc A LOT.

Side Note: I'm finding I don't have much preference over embedding the code vs using Gist; so I'll probably do more embedding. A bit reason for this is that if I wanna review my posts at work; can't see the code. Gist is blocked... sigh

My notes for this post mention doing Unit Tests and then removing them when the addition of Functional Tests duplicate the behavior being tested... but in this case; the Unit Test IS a Functional Test. It's cleaner to implement/maintain.

There is the slight issue of ... there not being any complicated code in ItemAccess...

public class ItemAccess {
    public void topStories(Callback<Items> callback) {
        new ItemNetwork().topStories().enqueue(callback);
    }
}

It knows where to go for the actual call and how to execute it. If we go to implement a synchronous version; it'd wrap the async; but the ItemAccess is what knows how to do that. No where else considers or controls a|synch behavior.

It has behavior; but to apply unit tests really starts to hit in the implementation testing; which I want to avoid.

Summary

This feels like the end of the post. I have a Functional Test around the network layer. From the point the rest of the application will interface with, ItemAccess to the Callback invoked. That's as functional as the network layer tests can be.

What I want to move onto now is an AppiumTest. I want to do an integration test with the app. This will require a bit of the View-Bridge-Mediator to be hacked into existence; but that'll be fine since we're looking at a very small scope, and we practice merciless refactoring.

Tune in next time for the Appium treatment!

All the code up to this point is available for review here which is tied to the hash; so always the code at the end of this post. The 7 commits from this post don't have very useful comments... I'll try to resolve that for the next post.

Show Comments