lundi 29 avril 2019

How to Unit test runtime-specific interface implementations with minimal duplication

I have a Xamarin solution in VS2017, which implements a physical device a few times (over usb/serial and bluetooth) on Windows UWP and Android. These devices all implement IMyDevice, which defines a simple state machine that models MyDevice being connected or not, and measuring or not.

I test these implementations using Xunit, in their respective namespaces, and since they all implement IMyDevice, I get quite a few tests that are literally identical, and currently repeated 3 times. I will soon add more devices, and must find a way to prevent this repetition from getting completely out of hand.

To test my view models, I build an assembly in a separate, common namespace and include it in my various unit test projects, using:

AddTestAssembly(typeof(MyViewModel).GetTypeInfo().Assembly);

This works for view models because I don't need any platform specific types, but alas, I don't think I'll be able to pass a MyDevice implementation to an assembly that is already built!

A more feasible approach would be to use Xamarin's dependency service

DependencyService.Get<MyDevice>();

However, it seems conceptually wrong to me that my Serial/Bluetooth unit tests should require Xamarin forms. And I'm not sure I'd be done anyway because I'd still need to disambiguate my serial and Bluetooth device on windows.

Therefore I wrote it out long-hand. I was Using Xuint in two projects/namespaces like:

namespace Test.MyApp.UWP {
    public class TestMyBluetoothDeviceUWP {
        [Fact]
        void DeviceDoesntSuck() {
            Assert.False(new MyUWPBluetoothDevice().sucks());
        }
    }
    public class TestMySerialDeviceUWP {
        [Fact]
        void DeviceDoesntSuck() {
            Assert.False(new MyUWPSerialDevice().sucks());
        }
    }
}

namespace Test.MyApp.Droid{
    public class TestMyDeviceDroid {
        [Fact]
        void DeviceDoesntSuck() {
            Assert.False(new MyDeviceDroid().sucks());
        }
    }
}

The popular answer to questions similar to mine seems to be using a [Theory]

namespace Test.MyApp{
    public class TestMyDevice {
        [Theory]
        [ClassData new MyUWPBluetoothDevice()]
        [ClassData new MyUWPSerialDevice()]
        [ClassData new MyDeviceDroid()]
        void DeviceDoesntSuck(IMyDevice myDevice) {
            Assert.False(myDevice.sucks());
        }
    }
}

this helps with devices defined within the same namespace, however it does not help with the repetition across test projects (namespaces)

so far the best solution I can think of (which is not that great) is to define the tests statically, and wrap them in callers in the test projects

namespace Test.MyApp{
    public static class TestMyDevice {
        static void DeviceDoesntSuck(IMyDevice myDevice) {
            Assert.False(myDevice.sucks());
        }
    }
}

namespace Test.MyApp.UWP {
    public class TestMyDeviceUWP {
        [Theory]
        [ClassData new MyUWPBluetoothDevice()]
        [ClassData new MyUWPSerialDevice()]
        void DeviceDoesntSuck(IMyDevice myDevice) {
            Test.MyApp.TestMyDevice.DeviceDoesntSuck(myDevice);
        }
    }
}

namespace Test.MyApp.Droid{
    public class TestMyDeviceDroid {
        [Fact]
        void DeviceDoesntSuck() {
            Test.MyApp.TestMyDevice.DeviceDoesntSuck(new MyDeviceDroid());
        }
    }
}

this is more maintainable since I only have one implementation of each test, but it is still a lot of duplication since I have to wrap each one on each platform

In summary, if anyone knows how to: run a whole battery of tests against different runtime-dependant implementations of an interface that live in different projects/namespaces, I'd be very interested to hear what you have to say.

Aucun commentaire:

Enregistrer un commentaire