【问题标题】:Using MSTest and Fakes (Shim) to shim the .Net System.Windows.Forms.Screen constructor for unit testing使用 MSTest 和 Fakes (Shim) 填充 .Net System.Windows.Forms.Screen 构造函数以进行单元测试
【发布时间】:2014-12-11 11:35:09
【问题描述】:

我在做什么

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

/// <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 类的其他实现。只要我可以替换(填充)屏幕类的构造函数以将边界字段设置为我将在我的设置中提供的字段,我将是金色的,对吗?
除非在这里有人会发现我的推理有缺陷!


注意,虽然我很欣赏这里的一些人有不同的意见和观点,但我会谦虚地要求您保持谦虚并以建设性的方式提出您的论点。如果我做错了什么,请告诉我如何解决这个错误。
我一次又一次地在 SE 网络上提问时,有人说我错了,但没有建议我如何才能变得正确。感谢您的考虑。

【问题讨论】:

  • 您只需要测试这一种方法吗?在这种情况下,您应该为每个测试用例制作模拟对象并将其传递给您的方法。您需要模拟 Bounds 和 AllScreens 属性。
  • @gabba 你能给我举个例子来说明我如何模拟 AllScreens 属性,我还需要实例化Screen 对象吗?

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


【解决方案1】:

我的解决方案

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

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

好吧,图片和标题是的,你没听错,创建一个对象而不调用任何构造函数。 还有文字……

在执行的这一点上,僵尸对象将跃入生命,没有灵魂(或就此而言的状态)。

您应该关心的第一件事是为私有字段插入一些值,这些值将是 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);
}

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

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

将我自己的答案标记为解决方案。到目前为止,它创造了奇迹。如果它坏了会通知你。

【讨论】:

    【解决方案2】:

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

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

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

    因此您可以使用自定义逻辑在任何地方传递模拟。

    直接使用ScreenBoundsWrapper insted of use screen,这样你就可以在没有真实屏幕的情况下随意为测试用例创建对象。

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

    【讨论】:

    • 感谢您的回答。一开始的那个不等式,你能说得更清楚一点吗?我当前的测试实现表明所有变体都有效。至于制作完整的包装器,我确实在无法模拟 Screen 类的情况下考虑了它。我什至考虑从头开始实现我自己的,使用本机调用,因为这样会减少我的代码浪费。我不喜欢仅仅因为缺少一个小功能而包装的包装器。该扩展方法的目标是无论如何扩展 .Net 库,而不是提供自定义实现。
    • @Cadde 对不起,我的错,我以为 y 轴会上升 ;)
    • 我同意你的观点,在你的情况下,不使用包装器更容易。但在某些情况下,包装第三方代码并提供应用程序所需的功能并不是一个坏主意。但这应该始终是基于努力、时间和利润的平衡决定。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-21
    • 2012-06-28
    • 1970-01-01
    • 2015-09-18
    • 1970-01-01
    相关资源
    最近更新 更多