Thursday, September 28, 2017

Unit testing Xamarin.Forms Behaviors

So in my last post, I outlined how I’m unit testing my Xamarin.Forms projects. Today, I want to highlight a practical example of unit testing a Behavior.

Let’s take a simple behavior that prevents item selection in a ListView:

public class DisableListViewSelection : Behavior<ListView>
{
    private ListView _attached;

    protected override void OnAttachedTo(ListView bindable)
    {
        _attached = bindable;

        if (_attached != null)
        {
            _attached.ItemSelected += Bindable_ItemSelected;
        }
    }


    protected override void OnDetachingFrom(ListView bindable)
    {
        if (_attached != null)
        {
            _attached.ItemSelected -= Bindable_ItemSelected;
        }
    }

    private void Bindable_ItemSelected(object sender, SelectedItemChangedEventArgs e)
    {
        _attached.SelectedItem = null;
    }
}

Unit testing the behavior should be straight forward but there are a few gotchas.

The first concern is that we're testing a visual that requires Xamarin.Forms to be initialized using Xamarin.Forms.Forms.Init();. This is easily addressed using the Xamarin.Forms.Mocks nuget package I mentioned in my last post.

The second concern is that the Behavior<T> implementation explicitly implements the IAttachedObject interface which is marked as internal. We can address this with some Reflection hackery.

I’ve addressed both concerns with the following base test fixture:

public abstract class BaseBehaviorTests<TSubjectBehavior, TTargetElement> : INotifyPropertyChanged
    where TSubjectBehavior : Behavior<TTargetElement>, new() 
    where TTargetElement : BindableObject, new()
{
    private BindingFlags _bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance;

    public TTargetElement ContainingElement { get; set; }
    public TSubjectBehavior Subject { get; set; }

    public event PropertyChangedEventHandler PropertyChanged;

    public virtual void Setup()
    {
        Xamarin.Forms.Mocks.MockForms.Init();

        Subject = new TSubjectBehavior();
        ContainingElement = new TTargetElement();
    }

    protected virtual void Attach()
    {
        Subject.GetType().GetMethod("Xamarin.Forms.IAttachedObject.AttachTo", _bindingFlags).Invoke(Subject, new object[] { ContainingElement });
    }

    protected virtual void Detach()
    {
        Subject.GetType().GetMethod("Xamarin.Forms.IAttachedObject.DetachFrom", _bindingFlags).Invoke(Subject, new object[] { ContainingElement });
    }

    protected void NotifyPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Take a quick peek at the Attach/Detach methods. The IAttachedObject.AttachTo method is explicitly implemented on the class so we have to use the full namespace of the method to resolve it. If it was implicitly implemented, we could simply use "AttachTo".

Now that we have this test fixture capability, writing a test for our DisableListViewSelectionBehavior is dead simple:

[TestClass]
public class DisableListViewSelectionBehaviorTests : BaseBehaviorTests<DisableListViewSelection, ListView>
{
    List<object> _list = new List<object>();

    [TestInitialize]
    public override void Setup()
    {
        _list.Add(new object());

        base.Setup();
    }

    [TestMethod]
    public void WhenSelectingItem_AndAttachedToBehavior_ShouldUnselectedItem()
    {
	// arrange
	Attach();
        ContainingElement.ItemsSource = _list;

	// act
        ContainingElement.SelectedItem = _list.First();

	// assert
        ContainingElement.SelectedItem.ShouldBeNull();
    }

    [TestMethod]
    public void WhenSelectingItem_AndDetachedFromBehavior_ShouldKeepSelectedItem()
    {
	// arrange
        Detach();
        ContainingElement.ItemsSource = _list;

	// act
        ContainingElement.SelectedItem = _list.First();

	// assert
        ContainingElement.SelectedItem.ShouldNotBeNull();
    }
}


Well, that's all for now. My next post will look at behaviors with data binding.

Happy coding!

No comments:

Post a Comment