HackerNews UWP : Getting Some Data
When we last left our brave little app... And if it's trying to be on the windows platform; you know it's indeed brave - It had just configured it's network layer and is looking to expose it to the wider world!
I spent a few hours staring at the black magic of Refit and found how it's creating the instantiated interface. And that it'd be a bit of a pain to either do it myself (oh man would there be lots of borrowing) or to update to include a Convertor factory. I'd still like to do both of those things; but... I don't know how to get the Nustashe black magic to work. And... at the moment... I'm ok with that.
What my plan is for now; in the Internal
namespace; have the ItemJson
be the returned type; and do a custom adapter in the HackerNewsNetwork
. I like Retrofit's way of doing the adapter... I don't have it; and am not currently going to do the time sink to add it. I have a lot of projects and ideas... My adaptation of RetroFit for C# is now one of them... ... Yay...
OK, back to the actual project!
Earlier today I got myself a 4K monitor... Oh man... Things might be hard to read; but literally 4X the screen space... This is AWESOME!!! ... Right... Coding...
Using Explicit Convertor
My plan to introduce a convertor in the HackerNewsNetwork
requires some changes to the tests.
Errrr... test.
I've updated the test HackerNewsNetworkTests#ShouldReturnTaskStories()
to be
[TestMethod]
public void ShouldReturnTaskItems()
{
FakeResponseHandler fakeResponseHandler = new FakeResponseHandler();
fakeResponseHandler.AddFakeResponse(new Uri($"{HostUrl}/topstories.json"), new HttpResponseMessage(HttpStatusCode.OK){Content = new StringContent(@"{""id"":123}")});
Task<ItemJson> taskStories = new HackerNewsNetwork(fakeResponseHandler).TopStories();
ItemJson actual = taskStories.Result;
actual.Id.Should().Be(123);
}
It should return a collection of Item
; in the Items
object. So; better named now. But requires a few iterations to get both the test and the code into the correct state.
For example... I have no item class.
I've updated the provided JSON to be a collection of items @"[{""id"":123},{""id"":1234}]"
which is now forcing the change from a single ItemJson
to an array of them.
From HackerNewsNetwork#TopStories
; we'll want to return Items
. I wonder if I can just create that class with a ctor w/param of ItemJson[]
. Which... Looking at the Android code; isn't the route I want to go. I need to have these are ItemId
s.
Refactored ItemJson
to ItemId
. Now to change the ItemId[]
to Items
.
Task<ItemId[]> taskStories = new HackerNewsNetwork(fakeResponseHandler).TopStories();
to
Task<Items> taskStories = new HackerNewsNetwork(fakeResponseHandler).TopStories();
and follow the cascading changes down.
I'm definitely cheating a bit knowing what works for the outcome. It's an advantage, but for the purposes of demonstrating XP practices; there's a bit cheating. This is more real world though; it's knowing the better solution; and utilizing that knowledge. Not thinking it'll be a solution we need; but a solution that we've done this with before.
Cheating bites the arse
So... I've been poking at this translation and started looking into setting up the HackerNewsAccess
class, to mirror Android... and... Android needed it to abstract the enqueue
mechanism... There's no such need in C#; everything is async/await. So; moving the HackerNewsNetwork
out of the Internal
and renaming to HackerNewsAccess
. I like the naming; as it allows contiguous understanding. Even if the deeper classes have changed. The HackerNewsAccess
is the API for accessing the hacker news data.
And... Hmmm... I need the CallBack
but since it's not actually doing a call back; instead of having the HackerNewsAccess
return the Items
it will return a Response<Items>
. I think. Well... it's the path I'm going down. Less because the codes driving me... I KNOW... but because I need the error data and just returning the 'happy path' object isn't sufficient.
Hmmm.... OK; my current path isn't going well.
Checking the previous project where I had gotten further... I see. The Refit interface returns the HttpResponseMessage
.
public interface IHackerNewsApi
{
[Get("/topstories.json")]
Task<HttpResponseMessage> TopStories();
}
That is then put into the Response
to be able to build from.
The downside here; it removes my plan around a custom adapter from ItemId[]
to Items
. It does introduce the possibility of adding custom adapters though...
Return of Custom Adapters!
OK, so I think I can get custom adapters back in. Which I think I need to do to get the HackerNewsAccessTests#ShouldReturnTaskItems
to pass. it's blowing up trying to return Items
which makes sense since there's no way for it to set those.
Here's the current test
[TestMethod]
public async Task ShouldReturnTaskItems()
{
FakeResponseHandler fakeResponseHandler = new FakeResponseHandler();
fakeResponseHandler.AddFakeResponse(new Uri($"{HostUrl}/topstories.json"), new HttpResponseMessage(HttpStatusCode.OK){Content = new StringContent(@"[{""id"":123},{""id"":1234}]") });
Response<Items> response = await new HackerNewsAccess(fakeResponseHandler).TopStories();
int count = response.Body().Count();
count.Should().Be(2);
}
which is failing due to not being able to parse into an Items
Message: Test method HackerNewsUwp.Tests.Network.HackerNewsAccessTests.ShouldReturnTaskItems threw exception:
Newtonsoft.Json.JsonSerializationException: Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'HackerNewsUwp.Network.Items' because the type requires a JSON object (e.g. {"name":"value"}) to deserialize correctly.
This is the invoked method; and where I think we can cleanly introduce the adapters.
At least initially... I might evolve out the HackerNewsNetwork
as the 'adapter layer'. Gotta see how it looks.
public async Task<Response<Items>> TopStories()
{
IHackerNewsApi hackerNewsApi = RestService.For<IHackerNewsApi>(HostUrl,
new RefitSettings() {HttpMessageHandlerFactory = () => _messageHandler });
try
{
return new Response<Items>(await hackerNewsApi.TopStories(), null);
}
catch (ApiException apiException)
{
return new Response<Items>(null, apiException);
}
}
I'm creating a super simple adapter for Items
. Oddly named ItemsAdapter
public class ItemsAdapter
{
public Items ToObject(ItemId[] itemIds)
{
return new Items(itemIds);
}
}
I expect this to end up with an interface; but not yet, so... Moving on.
This was developed by a nice simple test
[TestMethod]
public void ShouldCreateItemsCallingToObject()
{
// Arrange
ItemId[] itemIds = new ItemId[0];
// Act
Items items = new ItemsAdapter().ToObject(itemIds);
// Assert
items.Should().NotBeNull();
}
This test went through a bit of refactoring itself. Tests are Production Code. So refactor and keep them clean. A crufty and messy test suite makes it hard to understand what broke; or if multiple broke because they are actually duplicate tests - Refactor... ruthlessly.
A bit more refactoring gets an ItemsAdapter.cs file into the main project.
Run the tests as my lazy navigation back to where I need to utilize the adapter.
I had to bounce around a bit before the code pointed out what the adapter looked like. I had a few extra generics... but nope. Doesn't need it. Uses the same generic as the Response
... Kinda a 'duh'; but other than that, I think it's a slick implementation.
Lots of time passes
I'm not sure when the last time I was working on this was. I've only been working on it at home; which eats into my relaxing time.
I'm back at the cheer studio on a shiny new laptop. The only complaint with the laptop is with the "Home" key being right next to the "backspace"... sometimes I get to the front of the line the wrong way.
I've bought a windows laptop so I can do UWP development in the environment I find I do a lot of blog projects; sitting while my daughter does cheer. My Ubuntu machine doesn't do the UWP so well... I'm looking at you Rider... and until then... new shiny!
reviews the blog post to see what I was doing
HA! I got the 4K monitor when I started... the new laptop has a 4K screen... I kinda love the real estate. I'm finding it funny the first post on each of the 4K screens is for the UWP app; more specifically - WORK ON THE SAME POST!!!
I should have left it with a failing test...
Looks like I was just creating the object to handle the Item json payload; via an ItemId
class. Closely following the Android model. Is this the right model? No idea. Is it what I know to write towards? Yes.
Working software wins. Getting the right model is not as important as getting the right enough model. We'll refactor if it's needs improvement. If it doesn't need improvement; then it is, indeed, the perfect model. We don't know that no matter how much time we spend doing BUFD.
AND - Just got Visual Studio installed and open on the new laptop... ReSharper!!!
Gotta fix this. (I love you JetBrains; please give me free things... ... yeah... like TeamCity...)
Let's run the test!
They pass!
Shocked... I try to check them in passing.
On the nice side - ReSharper is running UWP now. I don't know if it's the other system, or the updates. I'll check that later.
I see this little test
[TestMethod]
public void BodyShouldSomethingGivenFailure()
{
ItemJson itemJson = new Response(new HttpResponseMessage(HttpStatusCode.Unauthorized), null).Body();
itemJson.Should().BeNull();
}
which... Maybe it should get a better name.
Time to open up AndroidStudio and get reminders of choices I already made...
sigh
Because I find it so much fun to do... guess what I didn't push to GitHub...
Well... It explains why the code didn't make a lot of sense in it's current state. I'm expecting more to be accomplished. Which... I'm saying I think there is... but I'm not going to rule out not having done much more.
I know I'm missing code because the test I reference before my return ShouldCreateItemsCallingToObject
isn't in the code I have available.
So... FINE... Gonna go back to NOT doing UWP.
time passes
I tried again. My new laptop ins't a fan of the gym's network. I'll just have to be sure to pull code and google via phone.
ANYWAY - Back to trying to do something... Let's ... ONCE AGAIN - review the code to figure out where I was/am.
Looks like I'm building out the Items
class.
namespace HackerNewsUwp.Tests.Network
{
[TestClass]
public class ItemsTests
{
[TestMethod]
public void ConstructorThrowsArgumentExceptionGivenNull()
{
Action action = () => new Items(null);
action.ShouldThrow<ArgumentNullException>();
}
[TestMethod]
public void CountReturnsCountOfItems()
{
int count = new Items(new ItemId[0]).Count();
count.Should().Be(0);//This should fail
}
}
}
What I'm looking at now is that I'm building up the same system I've built in android; in, largely, the same fashion.
Do I want to do this as just a "speedier" implementation?
A thought is that I could start higher level. I can build the UI; write a test expecting fields to be certain values.
Build the systems to support that...
I'd like to see how that flows.
AND - I'm gonna be hitting the XAML a little sooner. ... I tried; it broke.
I'm exploring the Items
a little more; I'm not sure where to go; what to test. I can expose a lot of stuff... but unless I'm starting to just 100% duplicate the android code; I'm not really TDDing this. Which I don't like.
I need a UI now... First UI will be just A story, just a title.
... Really?
public class TextBox : Control, ITextBox, ITextBox2, ITextBox3, ITextBox4
Really? WTF...
Knowing C#; my hack on Android to set text isn't gonna work here. I might be stuck with UI controls... OH!! OH!!! I'm in C#!!! I can use partial
classes!!!!!
I'll have to see how that looks... once I'm there.
Well... I think the early work on the UI is good. I basically gotta figure out the unit test UI interaction like I did for Android. While the test are currently running and starting the App; it gets angry when I try to write to the UI thread
Test method HackerNewsUwp.Tests.Screens.MainPage.MainPageBridgeTests.BridgeShouldTakeIMainPage threw exception:
System.Exception: The application called an interface that was marshalled for a different thread. (Exception from HRESULT: 0x8001010E (RPC_E_WRONG_THREAD)). If you are using UI objects in test consider using [UITestMethod] attribute instead of [TestMethod] to execute test in UI thread.
at Windows.UI.Xaml.Controls.TextBox..ctor()
at HackerNewsUwp.Tests.Screens.MainPage.FakeMainPage..ctor()
at HackerNewsUwp.Tests.Screens.MainPage.MainPageBridgeTests.BridgeShouldTakeIMainPage()
I wonder... can I do a similar cheat as android?
While the Android version utilized the super classes method; if C# uses it to just BE an interface... then internal to things; it's JUST an interface...
Here's what I have:
public interface IText
{
string Text { set; }
}
public class FakeText : IText
{
public string Text { get; set; }
}
public class FakeMainPage : MainPageBridge.IMainPage
{
internal FakeText TxtFakeText = new FakeText();
public IText Title()
{
return TxtFakeText;
}
}
And it gets the test passing. I'll have to have the QacTextBox
. (if you check the URL of the blog; probably gonna by QgTextBox
)
My test works. I can UT elements around the UI. I probably should look at doing a non-UWP libary so it doesn't RUN THE APP to run the code. The UWP being a literal shell... Maybe. For now I accept the app running.
Here's my quick little hack to get UT the code for displaying a title
[TestClass]
public class MainPageBridgeTests
{
[TestMethod]
public void ShouldSetTextOnTitle()
{
FakeMainPage fakeMainPage = new FakeMainPage();
MainPageBridge mainPageBridge = new MainPageBridge(fakeMainPage);
mainPageBridge.DisplayTitle("My Example Text");
fakeMainPage.TxtFakeText.Text.Should().Be("My Example Text");
}
}
public class FakeText : IText
{
public string Text { get; set; }
}
public class FakeMainPage : MainPageBridge.IMainPage
{
internal FakeText TxtFakeText = new FakeText();
public IText Title()
{
return TxtFakeText;
}
}
This probably isn't anywhere near where I was expecting it to be when I started. But ... I GOT MY SetText
INTERFACE!!! I'm thrilled about that.