µObjects: Unit Testable UI Interactions

This is a follow up to the Hotel Pattern which I've pretty much abandoned, though it's concepts and ideas have evolved into my current practices. It ties into the Interface Overload mechanism I discovered as well. Though, neither fit well into the new µObject paradigm that I'm developing in.

Both of those links are going to be useful to understanding what I'm striving for here in the big picture. Possibly a few of the posts they link out to as well.

The prompt for cranking this out in a single night was a twitter conversation where I hold on "No Getters".

Fully Testable UI Interfactions

One of the hallmarks of the Hotel Pattern is that ALL UI interaction is testable. All of it.

When I claim that all logic is under test, I mean all. Writing text to the ui. Changing visibility of a UI control - ALL TESTED.

The UI is a bookend. Wrap it.

I'll have more indepth technical practices posts coming where I dig into this deeper; but for now - let's just go with how to make your UI interactions 100% tested.


Encapsulate Behavior

My first example of wrapping the UI is going to be for changing visibility. We'll go with the Universal Windows Platform (UWP) as an example, because that's what I have on hand while writing this post, and I don't feel like spinning up android. :)

First, let's look at a simple example of how things are normally written.

public sealed partial class SamplePage : ISamplePage
{
    ...
    private async void BtnHide_OnClick(object sender, RoutedEventArgs e){
        SomeUiElement.Visibility = Visibility.Collapsed;
    }
    ...
}

We can't test this. We can't instantiate the page. We can't be sure that it continues to do what it should without the long and painful CodedUiTests.

MAYBE it's simple enough we don't need to worry about it... No. Logic must be under test.
Since we can't instantiate the page, the logic must be elsewhere.
Enter the command pattern; I'm kinda guessing that this is the right name... Seemed close enough at some point, so I'm using it. Please feel free to correct me to what I'm actually using.

public sealed partial class SamplePage : ISamplePage
{
    ...
    private void BtnHide_OnClick(object sender, RoutedEventArgs e){
        _hideCommand.Act(this);
    }
    ...
}

We're still doing something; but it's not logic. It's a method call and an argument passed. These are language constructs enforced by compile time. Being overly concerned with this means we're testing the fundamental assumptions we assume to be able to write the code. That's silly - don't. It's like testing a getter; it's a getter. Also never getters.

The second point is that the UI layer is a, largely, untestable layer. We must avoid any logic, and only perform language functionality.

If we're invoking some command pattern; how does it actually hide things?

So glad you asked.

public sealed class HideCommand : IHideCommand{
    ...
    public void Act(ISamplePage samplePage){
        samplePage.SomeElementVisibility().Hide();
    }
}

I bet samplePage.SomeElementVisibility().Hide() seems pretty damn strange. Let's introduce another aspect of my ancient Hotel Pattern. The View layer ONLY returns controls, or delegates calls. It does ZERO logic, because the View is not unit testable.

To do this, we introduce a wrapper of the untestable. It's our UI Bookend for visibility.

public sealed class VisibilityOf : IVisibility
{
    private readonly UIElement _uiElement;

    public VisibilityOf(UIElement uiElement) => _uiElement = uiElement;

    public void Show() => _uiElement.Visibility = Visibility.Visible;

    public void Hide() => _uiElement.Visibility = Visibility.Collapsed;
}


public interface IVisibility
{
    void Show();
    void Hide();
}

This class is considered untestable. It's dealing with 3rd party code (yes, OS/System is 3rd party). We assume they function.

How this is used, clearly though a method call samplePage.SomeElementVisibility().Hide() is like this

public sealed partial class SamplePage : ISamplePage
{
    ...
    private void BtnHide_OnClick(object sender, RoutedEventArgs e) => _hideCommand.Act(this);
    public IVisibility SomeElementVisibility() => new VisibilityOf(SomeUiElement);
    ...
}

SomeUiElement is NEVER exposed outside of the UI layer. ONLY an interface that exposes the BEHAVIOR we want to use. We can 100% test that HideCommand does what we want when we want.

How does this work for the prompting question - Almost identically. Almost, since if you try identically, you'll get the wrong implementation.

In UWP, it's a TextBlock. Sub in your favorite text control

public sealed class WriteTextBlock : IWriteText
{
    private readonly TextBlock _textBlock;

    public WriteTextBlock(TextBlock textBlock) => _textBlock = textBlock;

    public void Write(string text) => _textBlock.Text = text;
}
public interface IWriteText
{
    void Write(string text);
}

This is the class and interface we use to interact with behavior without exposing, or knowing about, the actual control.

Continuing to build on the existing page we'll add a method to expose SomeUiElement which... is ... now a TextBlock.

public sealed partial class SamplePage : ISamplePage
{
    ...
    private void BtnHide_OnClick(object sender, RoutedEventArgs e) => _hideCommand.Act(this);
    public IVisibility SomeUiElementVisibility() => new VisibilityOf(SomeUiElement);
    public IWriteText SomeUiElementText() => new WriteTextBlock(SomeUiElement);
    ...
}

This is where the difference comes into play. Following the VisibilityOf model requires getters.

public sealed class NotDataBag : INotDataBag{
    private string _someValue;
    public string SomeValue() => _someValue;
}
public sealed class WriteTextCommand : IWriteTextCommand{
    ...
    public void Act(ISamplePage samplePage){
        samplePage.SomeUiElementText().Write(_notDataBag.SomeValue());
    }
}

The whole point is to not have databags. Let's evaluate this situation. This is the exact same type of practice I take developers through when exposing them to the concept.

If we're getting data out of an object - Why? We're doing something with it; there's some behavior - That should be encapsulated into the object with the data.

Here; we're wanting to display things. So... Let the object set the value. We're using interfaces, so our objects never need to know about the UI; or what the UI is... or even if it IS actually a UI (though abuse of that nature, I'm gonna frown at).

Let's encapsulate that behavior into our object

public sealed class NotDataBag : INotDataBag{
    private string _someValue;
    
    public void SomeValueInto(IWriteText item) => item.Write(_someValue);
}

The behavior is encapsulated. The data is never returned. Our objects own the behavior around our objects. We now never have to re-write the display code for _someValue. Written once, and only once. This is the huge win of actual object oriented programming - things are only written once, even the tiny things.

The command we have writing will then look like

public sealed class WriteTextCommand : IWriteTextCommand{
    ...
    public void Act(ISamplePage samplePage){
        _notDataBag.SomeValueInto(samplePage.SomeUiElementText()));
    }
}

That reads pretty well too, no? "notDataBag's SomeValue into simplePage's SomeUiElement".

And... that's my current itteration of how to display text in a fully testable fashion.

Show Comments