使用MSTest和Fakes(Shim)来填充.Net System.Windows.Forms.Screen构造函数以进行单元测试

时间:2014-12-11 11:35:09

标签: c# .net unit-testing microsoft-fakes

我在做什么

我编写了一个静态扩展方法,用于查找驻留在当前屏幕实例左/右/上方/下方的所有Screen实例。

/// <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;
}

对代码的建设性建议当然是受欢迎的。

我需要什么

我当然是对我的所有代码进行单元测试,在这种情况下我无法进行TDD(测试驱动开发),因为我无法理解应如何测试此操作。所以我编写了实现,希望在编写完成后能够搞清楚。

我仍然无法绕过这一个 由于Screen的.Net实现没有任何构造函数可以采用IScreen接口,也没有开始的IScreen接口,我将如何进行我的测试设置可以欺骗我有...在我的系统上连接10个以上的屏幕/显示器进行测试?

我查看了Microsoft Fakes个shim示例,但它仍然没有下沉 问题是,如何通过覆盖Screen constructor伪造10多个屏幕?

根据我的实现,我只需要屏幕界限,所以我不认为我需要担心.Net中Screen类的其他实现。只要我可以替换(shim)屏幕类的构造函数来将bounds字段设置为我将在我的设置中提供的那个我将是金色的,对吧? 当然,禁止某人在我的推理中发现了一个缺陷!


N.B ,虽然我很欣赏这里的一些人有不同的意见和看法,但我会谦卑地要求你保持谦虚,并以建设性的方式提出你的论点。如果我做错了,请告诉我如何解决这个问题 我一次又一次地在SE网络上提问,有人说我错了,却没有暗示我怎么能做对。谢谢你的考虑。

2 个答案:

答案 0 :(得分:2)

我的解决方案

在查看this blog entry(使用内部构造函数实例化类)后,我终于相信我弄明白了。
我正在挠头,因为没有施工人员可以看到它们是如何内部的。 所以是的,在我意识到/通过反射提醒之前,没有办法创建更多的Screen对象实例。一个人可以创造僵尸。未初始化的类,然后通过反射在实例上设置值。这允许人们直接设置私人成员的值,这正是我所需要的。

无论如何,这张照片让我意识到我正在寻找的是什么。在看到它之前,我感到迷失了另一个关于假货和测试的页面

Zombies

嗯,图片和标题是的,你没听错我,没有调用任何构造函数就创建了一个对象。 文字......

  

在执行的这一点上,僵尸对象将跳跃到生命中,没有灵魂(或状态)。

     

你应该关注的第一件事是插入私有字段的一些值,这些值将为null并执行构造函数可能具有的任何关键卷。

     

我强烈建议您在使用Reflector之类的工具中研究目标对象的构造函数,然后再自行初始化。

得到的测试方法

注意这是一个草稿,我打算稍后重新使用该模型进行其他测试。
我并不需要在实现中更改任何内容以保持不变。

[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);
}

像往常一样,我很乐意就我在实施和测试方法(ology)中可以改进的内容提出建议。

哦,作为奖励,这里是虚拟屏幕布局的样子,因为这也需要某种验证。 1/10比例。

Screen layout

将我自己的答案标记为解决方案。到目前为止,它创造奇迹。如果它破裂会告诉你。

答案 1 :(得分:1)

抱歉,由于许可证问题,我无法使用MS Shims编写示例。

我认为如何改进实现的一种方法是包装所有低级API。 使用ScreenFactory,直接使用AllScreens属性:

public class ScreensFactory
{
    public List<ScreenBoundsWrapper> GetAllScreens()
    {
        return Screen.AllScreens
            .Select(s => new ScreenBoundsWrapper(s))
            .ToList();
    }
} 

所以你可以通过自定义逻辑模拟传递到处。

直接使用ScreenBoundsWrapper使用屏幕,因此您可以随意为没有真实屏幕的测试用例创建对象。

public class ScreenBoundsWrapper
{
    public ScreenBoundsWrapper()
    {            
    }

    public ScreenBoundsWrapper(Screen screen)
    {
        screenInstance = screen;
    }

    public Screen ScreenInstance
    {
        get { return screenInstance; }
    }

    public virtual Rectangle Bounds
    {
        get { return ScreenInstance.Bounds; }
    }

    public override bool Equals(object obj)
    {
        var w = obj as ScreenBoundsWrapper;
        if (w != null)
        {
            return w.ScreenInstance.Equals(screenInstance);
        }

        return obj.Equals(this);
    }

    protected bool Equals(ScreenBoundsWrapper other)
    {
        return Equals(screenInstance, other.screenInstance);
    }

    public override int GetHashCode()
    {
        return screenInstance == null ? 0 : screenInstance.GetHashCode();
    }

    private readonly Screen screenInstance;
}

我更改了您的扩展方法,例如:

public static class Extensions
{
    public static Collection<ScreenBoundsWrapper> FindAllScreens(
        this ScreenBoundsWrapper source, 
        ScreensFactory factory, 
        ScreenSearchDirections directions,
        params ScreenBoundsWrapper[] 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<ScreenBoundsWrapper>(excludeScreens) { source }.ToArray();

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

        var allScreens = factory.GetAllScreens();


        allScreens.RemoveAll(excludeScreens.Contains);

        var result = new Collection<ScreenBoundsWrapper>();
        foreach (var screenWraper in allScreens)
        {
            // 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) && screenWraper.Bounds.Right <= source.Bounds.Left)
                result.Add(screenWraper);
            else if (directions.HasFlag(ScreenSearchDirections.Right) && screenWraper.Bounds.Left >= source.Bounds.Right)
                result.Add(screenWraper);
            else if (directions.HasFlag(ScreenSearchDirections.Above) && screenWraper.Bounds.Bottom >= source.Bounds.Top)
                result.Add(screenWraper);
            else if (directions.HasFlag(ScreenSearchDirections.Below) && screenWraper.Bounds.Top <= source.Bounds.Bottom)
                result.Add(screenWraper);
        }
        return result;
    }

在测试项目中编写此测试方法(我使用moq库):

        [TestMethod]
        public void TestMethod1()
        {
            var s1 = MockScreenWraper(1, 1, 1, 1);
            var s2 = MockScreenWraper(1, 3, 1, 1);

            var list = new List<ScreenBoundsWrapper> { s1, s2 };

            var mockScreenFactory = new Mock<ScreensFactory>();
            mockScreenFactory
                .Setup(m => m.GetAllScreens())
                .Returns(() => list);

            var factory = mockScreenFactory.Object;

            var screenAbove = s1.FindAllScreens(factory, ScreenSearchDirections.Above);

            Assert.AreSame(screenAbove.First(), s2);
        }

        private static ScreenBoundsWrapper MockScreenWraper(int x, int y, int w, int h)
        {
            var mock = new Mock<ScreenBoundsWrapper>();
            mock.SetupGet(m => m.Bounds)
                .Returns(() => new Rectangle(x, y, w, h));

            mock.Setup(m => m.Equals(It.IsAny<ScreenBoundsWrapper>()))
                .Returns<ScreenBoundsWrapper>(
                    o => ReferenceEquals(mock.Object, o));

            return mock.Object;
        }

}