TDD against Android UI Elements

TDD against Android UI Elements

I've been working very hard to not accept excuses for not having tests on something. I want to be able to TDD as close to 100% of an Android app as possible. I currently have a few areas I need to develop techniques to enable TDDing.
One of these pain points was a class that had or used Android UI Elements. Since they fall into the "widget" package, I'll use "widget" sometimes.

The backstory to this is that I favor strong encapsulation of an object. That we can't ask for an objects data. We only get to ask if they can do something for us with their data.
An example is to display someone's first name in a TextView (using Android because it's platform I was working in). 98% of programmers will tell you to do something like

String firstName = userInfo.getFirstName();
userFirstNameTextView.setText(firstName);

This is bad design. In which it is not object oriented design. This isn't encapsulation; or data hiding. Sure; a getter ALLOWS changing the underlying type w/o changing the method's return type... How often have you? I almost NEVER changed an underlying type and left the getter return type alone. I have one memory from years ago stand out because I did. Once. In 4 years of android development. I left the getter type once.
But it was renamed. It returned an int and migrated to a double; resulting in a refactor to getThingAsInt(). Not sure it counts...

I don't think 2% of developers do encapsulation correct; I'm pretty damn pessimistic about this. I think the last 1.9% would get the firstName like

userFirstNameTextView.setText(userInfo.firstName);

The last few (and yes; I'm pulling numbers out of my ass in a pessimistic way) will try to do something that maintains encapsulation.

userInfo.writeFirstName(userFirstNameTextView);

The method will that write the first name into the widget.
This is object oriented encapsulation. The data is hidden. It never gets out.
Sure; you can get it from the TextView but that's no longer the problem of the userInfo object. It's not it's copy; it's not it's reference.

I've struggled with this for a few months now; having accepted an Object Oriented Design philosophy; because I could not find a way to maintain encapsulation AND the 'purity' of the class.
It's aligned with the concept of a Clean Architecture. The Entities should not have dependencies on "higher level" object. The UserInfo should not have reference to a Widget.
I'll be doing a post re-working the VBM app to follow the Clean Architecture better. It's a pattern that I try to support; Layers. I see a few places I can do better; and will work to incorporate it.
For the purpose of this post; We can assume that I'm having serious issues passing a UI level into an Entity level.
I had a work around for a while of having a UserInfo#firstName method (no get; it's returning the "firstname". Actually it returned a Stream; or a Scanner; I probably had both; I liked neither. I used these are the advice of Fred George; but it was put out as a last resort.
I was at a last resort. I had no way which didn't violate the Clean Architecture (I'm going to REALLY like having a name for this).

I hated it; but every other choice was worse.

At one point; I could no longer accept my Adapter having ~0% code coverage.
The process I went through to get 100% coverage on my adapter was written up earlier; and I'll avoid linking to it here; It has some details; and I'm trying to write up more of them up here. :)

What I found was that Java treats interfaces differently than I've seen before.
If a derived class implements an interface with a definition in a base class; the derived class does not need to implement the method.

Words are bad; here's some code.

class Base{
    public foo(String str){...}
}
interface Bar{
    foo(String str);
}
class Derived extends Base implements Bar{
//Intentionally nothing here
}

class Consumer{
    void passer(){
        caller(new Derived());
    }
    void caller(Bar bar){
        bar.foo();//WORKS!!!
    }
}

The reason this becomes HUGELY important; is painfully apparent when you attempt to Mock a TextView. It has a number of final methods; including setText and getText.
I can't mock out the functionality being used! This ... is sad.

I can't accept sad. Which is why this is one feature of Java I really like.

What I've done for setting the text in a TextView is something like the following

public interface SetText{
    void setText(CharSequence cs);
}
public QacTextView extends TextView implements SetText{}//It requires some ctors left out here

and now I have a testable ui control w/o dependency on the widget; which I couldn't have tested against anyway.

This does require using QacTextView in the layouts instead of TextView. A small price to pay for high confidence in the code.

How this ends up looking when put into practice (ecluding a few layers)

class UserInfo{
    public void writeFirstName(SetText item){
        item.setText(encapsulatedFirstName);
    }
}
class SomeActivity{
    void onEvent(){
        userInfo.writeFirstName(textViewFirstName);
    }
}

This is very explicitly missing a few layers; don't do code like this.
Except for the SetText interface stuff. :)

What we have here is an Entity class that takes in an interface. We can utilize Java's interface mechanism to avoid the method final in a base class. It doesn't remove it (like PowerMock) and it's not excused as "It's UI; that's a boundary, we can't test it". Which is what I did for a while.
It's just a ... bypass; for testing. We can't implement setText in our derived class, it's final.

I haven't done exhaustive work in this area; but I have done a few controls and a few methods - all worked.

This technique enable encapsulation and clean architecture.

What this technique provides is exactly what I've been looking for!
I'm actually pretty excited to share this. A few people at work I've shared this with think it's great and that it enables best practices to a wider range of code for Android development is fantastic.

If there's any questions about this; ping me on twitter @TheQuinnGil I'd love to discuss thoughts about it.

Show Comments