I've thought a bit about my TDD limitations on Android and my discovery of the GoLang-y nature of interfaces. For the TopStoriesAdapter.ViewHolder
to be tested; we can create a wrapper for other controls... Now I understand why Android didn't do it originally; performance; TDD was new... Lots of stuff.
Sooo.... I think I'm going to be developing my own set of Widgets with a growing interface (not doing it sweeping). It should enable TDD... but it'll also create a ton of "crap".
I don't really think this will be a recommended path for production code; where I'd recommend minimizing UI bound code; and test what can be via unit; and UI Test the rest.
This isn't production; this is me getting to do things my way. And... wow it's a bit of a round and round to get just the RecyclerView
interfaced up.
Ruthless Refactoring
I'm running tests to fix up some code coverage.
...
I didn't TDD a piece... :(
My failure to have TDD'd a chunk of code has caused 3 tests to fail when I got test coverage improved. This didn't change the production code. It just modified behavior of the tests; which broke 3 when I fixed 1. This shows that TDD even makes TESTS mode robust... ... Yea... TDD makes TESTS better...
I continue to make myself suffer the pangs of unpaired (and let's be honest; poor TDD) as it SO VERY PAINFULLY shows the power of power of pairing and TDD. Lessons I won't forget as it beats the importance of the practices into my head...
Excluding the Activities, wrapper-widgets, and unimplemented failure paths - I have 100/100 code coverage on all things except one... The Adapter
.
Which... kinda makes sense since that's what I spent a bunch of time writing wrapper classes for... before getting distracted by some RUTHLESS REFACTORING!!!
Testing The Adapter
The adapter needs testing. It needed a few wrapper classes. All of them so far, actually.
I'd actually never thought to test the Adapter. Well.... I thought to; then thought;
<whiny>but it's UI; that's hard to test. I can justify not doing it. UI is a boundary; it's OK</whiny>
In the end; No. It's not OK to "not test". YES there are exceptions; YES; you won't get 100/100 for EVERYTHING (nor should you REALLY care). But "hard" isn't a reason not to test. Challenging isn't a reason not to test. As The adapter isn't really a UI. It's just extending an android control which isn't testable. Which; BTW Google - FOR SHAME! The RecyclerView is plenty new that good practices should start to be applied!!!
I don't even have a null guard on the adapter... DEEP SIGH ... I'm better than that... I think/hope. :)
I'm working through some tests on the Adapter; I think I need to get the ViewHolder
down first...
Holy hell...
OK - I have a few widget wrappers... and the RecyclerView
is wrapped...
and with all the code updated to use these controls...
I ran it... and ... it... uhh... Worked.
Honestly; I did not expect that.
Now - Classes!!!
Since the UI wrapping works... I can start writing the backend code... yea... ... It makes sense.
I've been adding some of the classes to support the additional information in the StoryJson
.
Through this process I found a lot of commonality in the "Primitive Wrapper" classes. Which means I did a bunch of Copy & Paste.
Extracted the commonality into what I expect will grow into a nice collection of ObjectOrientedDesign base classes. I hope anyway. That way I have a structure to help direct my building.
In my refactor and wrapping of the UI; I have the QacTextView
with the setText
method; but I dislike the implied "UI Widget" being part of some baser classes. With a "SetText" interface with a "setText" method... starting to get into "mix-in" territory. I'll be keeping an eye on this kinda functionality to see if it can refactor into something with jargon.
I see some more common behavior with the duplicated code that may be simplified in another way besides the SimpleWrapper
... Gotta keep it in mind; but I don't have a good solution yet.
Unholy Hell
I made the UI wrapping stuff to be able to pass in mocked versions of the UI... Something I'd used in the past wasn't working. I didn't think much about it; I thought I had everything correct.
I didn't.
I think I'll have to roll back the wrapping changes. A fantastic learning experience; but... I don't think it is the correct path moving forward.
I've been wondering what I was missing such that the wrapping has never come up before - I found it. Just use byte manipulation to rip out the final stuff. Instead of making progress... refactor out the stuff I refactored in! Weeee...
Waste of Time
Was the changes trying to wrap the UI a waste of time?
No. Absolutely not.
I learned things. The Law of Two Feet says that if I feel I was learning; then it's an appropriate course of action to be on. I learned a new thing about Java. I explored it. It has potential in some situations.
I'm actually exploring using it in a more confined role. In my Object classes; like Author
public class Author extends SimpleWrapper<String> {
public final static Author NullAuthor = new Author("{{ Loading Author }}");
public Author(final String author) {
super(author);
}
@Override
protected void validate(final String author) {
if(Strings.isNullOrEmpty(author)){ throw new IllegalArgumentException("author can not be null");}
}
public void authorInto(final SetText item) {
if(item == null){ return; }
item.setText(value());
}
}
In particular the authorInto
(name in flux) method
public void authorInto(final SetText item) {
if(item == null){ return; }
item.setText(value());
}
SetText
is an interface with a signature matching TextView#setText
(creative naming; right?). This allows an object to have no dependency on UI widgets, but still able to interact with them.
This was the first interface I made for wrapping; to do exactly this.
As expanding to all controls is kinda out; I want to keep it in this limited focus to allow interactions without the UI dependency.
Will it work?
No idea
Should be fun to find out.
...
I've been trying to write tests for the ViewHolder
. I've been writing code towards this goal for about 12 hours. I still have no tests.
Upside; I don't feel any of it is wasted. A lot of learning and a better understanding. Plus a solid grasp of the technique I'm going to apply to be able to effectively test. I'm now set up to save hours whenever I encounter this type of situation again. 12 hours consumed; but uncountable hours saved from being able to test UI components... in theory. Gotta actually prove I can write those tests now.
...
Early results seem to support the wrapping for controls which get content set.
...
Continued work shows that the SetText
interface keeps the class API clean. There's no "UI Widget" in a "data bag" (not really data bag, but close enough for now)
...
Looking at setting up a # [minutes|hours|days] ago
and being lazy about doing the calculations ('cause someone's figured it out already) and found that android already has this via the android.text.format.DateUtils
; Specifically the getRelativeTimeSpanString
method.
I was implementing a translation from unix time to a Date
object; but via that method taking long
s; looks like I can skip that piece.
... frack... DateUtils
is a stubbed implementation. This does free us from some things because we can assume that it works. We don't need to test it.
Need to wrap this though.
It's an instance; much like the use of LayoutManager#from
in the TopStoriesAdapter
; that is tightly coupled to Android. We need to break this coupling. These are boundary points of the app which often require the exceptions which prove the rule.
I'll address the DateUtils first... in which I mean I'm going to frown at the fact that Google's gone the route of a static method... This is non-trivial to work around. Once I spend some time on it; we'll see how hard it is.
I'll need this same approach for the LayoutManager
. I understand using them in the early days (DateUtils
was added in API 3) when there was a significant concern over speed and memory; which a bunch of extra class instances would have affected... just a constraint at better development practices now.
FyzLog'd
My refactor work for logging in android is being applied here. I'm creating a wrapper around the android DateUtils
and creating slugs which I can flip from tests to do something different; but still something. It's clean; a bit odd, which I've been freely admitting. It's a boundary that can't be TDD'd otherwise. It's a requirement due to the limitations forced from the Android OS.
YAGNI
For the StoryComments; I'd included StoryId
in the constructor because I'll need it... later.
I ripped that out. It was sitting there doing nothing; and I had to re-arrange some code to test something else.
An unused piece of code was making me re-write and write differently... That's a paddling, now it's gone.
Working
All of the Story properties are being populated and displayed. This took a bit of working through setting up all the Objects representing these.
I'm really happy with all the work that's gone into setting up the UI testability. While I still need to approach this with the Activity; the Adapter is looking amazing.
The TopStoriesAdapter
which was at about 0% for method coverage and line coverage is now at 100% for both.
I said I'd need to re-work the LayoutManager
so the Adapter isn't tightly bound to it; ehhh.... I'm gonna hold off on it for now.
The only thing to test is either implementation or effects that would crash the app when running. The boundaries get a little flexibility; here's one I'm going to flex on.
Loading
All the data is loading.
FULL DATA ACHIEVED!
Learning Happened
I feel I have gotten a lot of value out of working through this post.
More than any other so far. There are some major points that stand out to me from everything I did through this post. They all fall into the "Object Oriented Design" bucket.
I've mentioned earlier in this post series of how I want to enforce encapsulation and not be returning data. This is in direct conflict of not polluting a class with dependencies on UI widgets.
I found a way to satisfy both. While the interface is ui-centric right now, easily adjusted. The usage of the wrapping class allows us to change the interface to something that isn't a signature match to UI widgets.
setAsString
? Sure. Now it's just a naming issue... and sometimes I'd rather work on caching.
The encapsulation I've been striving to find a satisfying answer to... done. I'm very pleased with this result.
I think it'll be a hard sell because "it's not default controls" ... Well... I have to fight for encapsulation first. :)
The encapsulation ties into the other aspect of Object Oriented Design; It massively simplifies the code.
If's exist only as guard clauses. This will change once I show why I call part of the pattern a Mediator
.
The DateUtils
adjustment at the end demonstrates the power of the design patterns to remove the need for branching logic.
If there's any desire to read up on the process I went through with that; read the Logger Refactor series.
An additional simplification of the code from using a lot of objects is allowing each object to focus on a single thing.
This is the "Single Responsibility" principle that people think they follow. In this case; the Story
class knows only about the "Story" concept. It knows it needs a "title" but not what makes a title a title. Just that when it's built; it can't be null.
Story
doesn't know that the value inside a title can't be null or empty
public class Title
...
@Override
protected void validate(final String input) {
if(Strings.isNullOrEmpty(input)) { throw new IllegalArgumentException("title can not be null or empty"); }
}
Story
doesn't need to know anything about how time is calculated and any conversions that need to occur. UnixTime? Why would a Story
need to know UnixTime?!
The correct answer is, "It doesn't". We've facilitated this by having a PostTime
class with the Single Responsibility of knowing how to take in the UnixTime and display it later.
I'm noticing there's some common behavior between classes where a MixIn approach might simplify things. I don't have enough examples demonstrating where it would make an improvement, which means that change must be held off.
While not the longest in the series so far; I feel I've learned the most from this one. I'm really pleased about that.