Search code examples
c#.netunit-testingmicrosoft-fakes

Using MSTest and Fakes (Shim) to shim the .Net System.Windows.Forms.Screen constructor for unit testing


What i am doing

I have written a static extension method that finds all Screen instances that reside to the left/right/above/below the current screen instance.

/// <summary>Finds all screens in the specified directions.</summary>
/// <param name="source">The screen to search around.</param>
/// <param name="directions">The directions to search in.</param>
/// <param name="excludeScreens">Any number of screens to exclude. The source screen is always excluded.</param>
/// <returns>A <see cref="T:Collection{T}"/> of <see cref="Screen"/> containing the found screens.</returns>
public static Collection<Screen> FindAll(this Screen source, ScreenSearchDirections directions, params Screen[] excludeScreens) {
    if (source == null)
        throw new ArgumentNullException("source");

    // Always exclude the source screen.
    if (excludeScreens == null)
        excludeScreens = new[] { source };
    else if (!excludeScreens.Contains(source))
        excludeScreens = new List<Screen>(excludeScreens) { source }.ToArray();

    // No direction is any direction.
    if (directions == ScreenSearchDirections.None)
        directions = ScreenSearchDirections.Any;

    var result = new Collection<Screen>();
    foreach (var screen in Screen.AllScreens.Where(screen => !excludeScreens.Contains(screen))) {
        // These are "else if" because otherwise we might find the same screen twice if our directions search for example left and above and the screen
        // satisfies both those conditions.
        if (directions.HasFlag(ScreenSearchDirections.Left) && screen.Bounds.Right <= source.Bounds.Left)
            result.Add(screen);
        else if (directions.HasFlag(ScreenSearchDirections.Right) && screen.Bounds.Left >= source.Bounds.Right)
            result.Add(screen);
        else if (directions.HasFlag(ScreenSearchDirections.Above) && screen.Bounds.Bottom <= source.Bounds.Top)
            result.Add(screen);
        else if (directions.HasFlag(ScreenSearchDirections.Below) && screen.Bounds.Top >= source.Bounds.Bottom)
            result.Add(screen);
    }
    return result;
}

Constructive suggestions on the code are of course welcome.

What i need

I am of course unit testing all my code, in this case i couldn't do TDD (Test Driven Development) because I simply can't wrap my head around how this operation should be tested. So i wrote the implementation in the hopes of figuring it out after having written it.

And i still can't wrap my head around this one.
Since the .Net implementation of Screen doesn't have any constructor that would take an IScreen interface, nor is there an IScreen interface to begin with, how would i go at doing the set up for my test where i could spoof that i have... say more than 10 screens/monitors attached to my system in any preferred layout for testing?

I have looked at Microsoft Fakes shim examples but it's still not sinking in.
The question is, how can i fake 10+ screens by overriding the Screen constructor?

As per my implementation, i am only going to need the screen bounds so i don't think i would need worry about the other implementations of the Screen class in .Net. As long as i can replace (shim) the constructor of the screen class to set the bounds field to one i would supply in my setup i would be golden, right?
Barring someone here finds a flaw in my reasoning of course!


N.B, while i appreciate that some people here have differing opinions and views, i would humbly request that you remain humble and formulate your arguments in a constructive manner. If i did something wrong then please tell me how i can fix that wrong.
I have time and time again, while asking questions on the SE network, had people say I am wrong without suggesting how i can become right. Thank you for your consideration.


Solution

  • My solution

    After looking at this blog entry (Instantiating Classes with Internal Constructors) i finally believe i got it figured out.
    I was scratching my head because there were no constructors to shim seeing as how they were internal. So yeah, there was no way to create more instances of the Screen object until i realized/was reminded that through reflection. One can create zombies. Uninitialized classes where you then set the values on the instances through reflection. This allows one to set values of private members directly of course, which is exactly what i needed.

    In any case, this picture made me realize exactly what it was i was looking for. Prior to seeing it i just felt lost reading yet another page about fakes and tests.

    Zombies

    Well, the picture and the heading Yes, you heard me correctly, create an object without calling any constructors. And the text...

    At this point in execution, the zombie object will leap to life, with no soul (or state for that matter).

    The first thing you should be concerned about is plugging in some values for the private fields, which will be null and performing any critical rolls the constructor would have.

    I strongly recommend studying the constructor of your target object in a tool such as Reflector, before initializing it yourself.

    The resultant test method

    Note that this is a draft, I intend to re-use the mockup for other tests later on.
    I didn't need to change anything in my implementation so that stays the same.

    [TestMethod]
    public void FindAll() {
        // Arrange: Create mock source screen and a bunch of mock screen objects that we will use to override (shim) the Screen.AllScreens property getter.
    
        // TODO: Move this to test class instanciation/setup.
        // A collection of 12 rectangles specifying the custom desktop layout to perform testing on. First one representing the primary screen.
        // In this list we imagine that all screens have the same DPI and that they are frameless.
        // Screens are ordered Primary...Quinternary, those marked ??? have not yet had an 'identifier' assigned to them.
        // Screens are named Primary for in front of user, then left of primary, right of primary, above primary and finally below primary. Closest screen to primary is selected.
        var screenBounds = new Rectangle[] {
            new Rectangle(0, 0, 2560, 1440),            // Primary screen. In front of the user.
            new Rectangle(-1920, 360, 1920, 1080),      // Secondary screen. Immediately left of the Primary screen. Lower edge aligned.
            new Rectangle(2560, 0, 2560, 1440),         // Tertriary screen. Immediately right of the Primary screen.
            new Rectangle(0, -720, 1280, 720),          // Quaternary screen. Immediately above the Primary screen, left aligned.
            new Rectangle(1280, -720, 1280, 720),       // ??? screen. Immediately above the Primary screen, right aligned. (This is side by side with the previous screen)
            new Rectangle(0, -2160, 2560, 1440),        // ??? screen. Above the Quaternary screen and it's neighbor. Spans both those screens.
            new Rectangle(-1920, -920, 960, 1280),      // ??? screen. Above the Secondary screen, tilted 90 degrees, left aligned.
            new Rectangle(-960, -920, 960, 1280),       // ??? screen. Above the Secondary screen, tilted 90 degrees, right aligned. (This is side by side with the previous screen)
            new Rectangle(0, 1440, 640, 480),           // Quinary screen. Immediately below the Primary screen, left aligned.
            new Rectangle(640, 1440, 640, 480),         // ??? screen. Immediately right of the Quinary screen and immediately below the Primary screen. (This is side by side with the previous screen)
            new Rectangle(1280, 1440, 640, 480),        // ??? screen. Immediately below the Primary screen and rigth of the previous screen.
            new Rectangle(1920, 1440, 640, 480),        // ??? screen. Immediately below the Primary screen and rigth of the previous screen.
        };
    
        // Create a bunch of mock Screen objects.
        var mockAllScreens = new Screen[12];
        var mockScreenBoundsField = typeof(Screen).GetField("bounds", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
        if (mockScreenBoundsField == null)
            throw new InvalidOperationException("Couldn't get the 'bounds' field on the 'Screen' class.");
    
        var mockScreenPrimaryField = typeof(Screen).GetField("primary", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
        if (mockScreenPrimaryField == null)
            throw new InvalidOperationException("Couldn't get the 'primary' field on the 'Screen' class.");
    
        var mockScreenHMonitorField = typeof(Screen).GetField("hmonitor", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
        if (mockScreenHMonitorField == null)
            throw new InvalidOperationException("Couldn't get the 'hmonitor' field on the 'Screen' class.");
    
        // TODO: Currently unused, create a collection of device names to assign from.
        var mockScreenDeviceNameField = typeof(Screen).GetField("deviceName", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
        if (mockScreenDeviceNameField == null)
            throw new InvalidOperationException("Couldn't get the 'deviceName' field on the 'Screen' class.");
    
        for (var mockScreenIndex = 0; mockScreenIndex < mockAllScreens.Length; mockScreenIndex++) {
            // Create an uninitialized Screen object.
            mockAllScreens[mockScreenIndex] = (Screen)FormatterServices.GetUninitializedObject(typeof(Screen));
            
            // Set the bounds of the Screen object.
            mockScreenBoundsField.SetValue(mockAllScreens[mockScreenIndex], screenBounds[mockScreenIndex]);
            
            // Set the hmonitor of the Screen object. We need this for the 'Equals' method to compare properly.
            // We don't need this value to be accurate, only different between screens.
            mockScreenHMonitorField.SetValue(mockAllScreens[mockScreenIndex], (IntPtr)mockScreenIndex);
    
            // If this is the first screen, it is also the primary screen in our setup.
            if (mockScreenIndex == 0)
                mockScreenPrimaryField.SetValue(mockAllScreens[mockScreenIndex], true);
        }
    
        // Act: Get all screens left of the primary display.
        Collection<Screen> result;
        using (ShimsContext.Create()) {
            ShimScreen.AllScreensGet = () => mockAllScreens;
            result = mockAllScreens[0].FindAll(ScreenSearchDirections.Left);
        }
    
        // Assert: Compare the result against the picked elements from our mocked screens.
        var expected = new Collection<Screen> { mockAllScreens[1], mockAllScreens[6], mockAllScreens[7] };
        CollectionAssert.AreEqual(expected, result);
    }
    

    As usual, i would happily take advice on what i could improve on both in my implementation and test method(ology).

    Oh and as a bonus, here's what the virtual screen layout looks like, because that needed some sort of validation as well. 1/10th scale.

    Screen layout

    Marked my own answer as the solution. It works wonders so far. Will let you know if it breaks.