由 mspec.exe 运行时 Spec 失败,但由 TD.NET 运行时通过

发布于 2024-08-22 13:39:32 字数 2899 浏览 9 评论 0 原文

我在另一个问题中写了关于这个主题的

然而,我已经重构了我的代码以摆脱配置访问,从而允许规范通过。或者说我是这么想的。它们使用 TestDriven.Net 在 Visual Studio 中运行良好。但是,当我在 rake 期间使用 mspec.exe 工具运行它们时,它们仍然会失败并出现序列化异常。因此,我创建了一个完全独立的示例,除了在线程上设置伪造的安全凭证之外,它基本上不执行任何操作。该测试在 TD.Net 中顺利通过,但在 mspec.exe 中失败。有人有什么建议吗?

更新:我发现了一个解决方法。研究该问题后,似乎原因是包含我的主要对象的程序集与 mspec.exe 不在同一文件夹中。当 mspec 创建一个新的 AppDomain 来运行我的规范时,该新的 AppDomain 必须加载具有主体对象的程序集才能反序列化它。该程序集与 mspec EXE 不在同一文件夹中,因此失败。如果我将程序集复制到与 mspec 相同的文件夹中,它就可以正常工作。

我还是不明白的是为什么ReSharper和TD.Net可以很好地运行测试?他们不使用 mspec.exe 来实际运行测试吗?

using System;
using System.Security.Principal;
using System.Threading;
using Machine.Specifications;

namespace MSpecTest
{
    [Subject(typeof(MyViewModel))]
    public class When_security_credentials_are_faked 
    {
        static MyViewModel SUT;

        Establish context = SetupFakeSecurityCredentials;

        Because of = () =>
            SUT = new MyViewModel();

        It should_be_initialized = () =>
            SUT.Initialized.ShouldBeTrue();

        static void SetupFakeSecurityCredentials()
        {
            Thread.CurrentPrincipal = CreatePrincipal(CreateIdentity());
        }

        static MyIdentity CreateIdentity()
        {
            return new MyIdentity(Environment.UserName, "None", true);
        }

        static MyPrincipal CreatePrincipal(MyIdentity identity)
        {
            return new MyPrincipal(identity);
        }
    }

    public class MyViewModel
    {
        public MyViewModel()
        {
            Initialized = true;
        }

        public bool Initialized { get; set; }
    }

    [Serializable]
    public class MyPrincipal : IPrincipal
    {
        private readonly MyIdentity _identity;

        public MyPrincipal(MyIdentity identity)
        {
            _identity = identity;
        }

        public bool IsInRole(string role)
        {
            return true;
        }

        public IIdentity Identity
        {
            get { return _identity; }
        }
    }

    [Serializable]
    public class MyIdentity : IIdentity
    {
        private readonly string _name;
        private readonly string _authenticationType;
        private readonly bool _isAuthenticated;

        public MyIdentity(string name, string authenticationType, bool isAuthenticated)
        {
            _name = name;
            _isAuthenticated = isAuthenticated;
            _authenticationType = authenticationType;
        }

        public string Name
        {
            get { return _name; }
        }

        public string AuthenticationType
        {
            get { return _authenticationType; }
        }

        public bool IsAuthenticated
        {
            get { return _isAuthenticated; }
        }
    }
}

I wrote about this topic in another question.

However, I've since refactored my code to get rid of configuration access, thus allowing the specs to pass. Or so I thought. They run fine from within Visual Studio using TestDriven.Net. However, when I run them during rake using the mspec.exe tool, they still fail with a serialization exception. So I've created a completely self-contained example that does basically nothing except setup fake security credentials on the thread. This test passes just fine in TD.Net, but blows up in mspec.exe. Does anybody have any suggestions?

Update: I've discovered a work-around. After researching the issue, it seems the cause is that the assembly containing my principal object is not in the same folder as the mspec.exe. When mspec creates a new AppDomain to run my specs, that new AppDomain has to load the assembly with the principal object in order to deserialize it. That assembly is not in the same folder as the mspec EXE, so it fails. If I copied my assembly into the same folder as mspec, it works fine.

What I still don't understand is why ReSharper and TD.Net can run the test just fine? Do they not use mspec.exe to actually run the tests?

using System;
using System.Security.Principal;
using System.Threading;
using Machine.Specifications;

namespace MSpecTest
{
    [Subject(typeof(MyViewModel))]
    public class When_security_credentials_are_faked 
    {
        static MyViewModel SUT;

        Establish context = SetupFakeSecurityCredentials;

        Because of = () =>
            SUT = new MyViewModel();

        It should_be_initialized = () =>
            SUT.Initialized.ShouldBeTrue();

        static void SetupFakeSecurityCredentials()
        {
            Thread.CurrentPrincipal = CreatePrincipal(CreateIdentity());
        }

        static MyIdentity CreateIdentity()
        {
            return new MyIdentity(Environment.UserName, "None", true);
        }

        static MyPrincipal CreatePrincipal(MyIdentity identity)
        {
            return new MyPrincipal(identity);
        }
    }

    public class MyViewModel
    {
        public MyViewModel()
        {
            Initialized = true;
        }

        public bool Initialized { get; set; }
    }

    [Serializable]
    public class MyPrincipal : IPrincipal
    {
        private readonly MyIdentity _identity;

        public MyPrincipal(MyIdentity identity)
        {
            _identity = identity;
        }

        public bool IsInRole(string role)
        {
            return true;
        }

        public IIdentity Identity
        {
            get { return _identity; }
        }
    }

    [Serializable]
    public class MyIdentity : IIdentity
    {
        private readonly string _name;
        private readonly string _authenticationType;
        private readonly bool _isAuthenticated;

        public MyIdentity(string name, string authenticationType, bool isAuthenticated)
        {
            _name = name;
            _isAuthenticated = isAuthenticated;
            _authenticationType = authenticationType;
        }

        public string Name
        {
            get { return _name; }
        }

        public string AuthenticationType
        {
            get { return _authenticationType; }
        }

        public bool IsAuthenticated
        {
            get { return _isAuthenticated; }
        }
    }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

迷你仙 2024-08-29 13:39:32

丹,

感谢您提供复制品。

首先,控制台运行程序的工作方式与 TestDriven.NET 和 ReSharper 运行程序不同。基本上,控制台运行程序必须执行更多的设置工作,因为它为每个运行的程序集创建一个新的 AppDomain(加上配置)。这是加载规范程序集的 .dll.config 文件所必需的。

每个规范程序集都会创建两个 AppDomain:

  1. 创建第一个 AppDomain (Console)
    当 mspec.exe 是隐式的
    执行后,
  2. mspec.exe 会为包含规范 (Spec) 的程序集创建第二个 AppDomain。

两个 AppDomain 通过 .NET Remoting 相互通信:例如,当在 Spec AppDomain 中执行规范时,它会将这一事实通知 Console AppDomain。当Console收到通知时,它会通过将规范信息写入控制台来采取相应的行动。

SpecConsole 之间的通信是通过 .NET Remoting 透明地实现的。 .NET Remoting 的一项属性是,在向目标 AppDomain (Console) 发送通知时,会自动包含调用 AppDomain (Spec) 的某些属性。 Thread.CurrentPrincipal 就是这样一个属性。您可以在这里阅读更多相关信息: http://sontek .vox.com/library/post/re-iprincipal-iidentity-ihttpmodule-serialized.html

您提供的上下文将在 Spec AppDomain 中运行。您在Because 中设置了Thread.CurrentPrincipalBecause 运行后,将向 Console AppDomain 发出通知。该通知将包含接收 Console AppDomain 尝试反序列化的自定义 MyPrincipal。它不能这样做,因为它不知道您的规范程序集(因为它不包含在其 私有 bin 路径)。

这就是为什么您必须将规范程序集放在与 mspec.exe 相同的文件夹中。

有两种可能的解决方法:

  1. MarshalByRefObject 派生 MyPrincipalMyIdentity,以便它们可以通过代理参与跨 AppDomain 通信(而不是正在序列化)
  2. 中暂时设置Thread.CurrentPrincipal因为

(格式化工作需要文本 - 请忽略)

Because of = () => 
{
    var previousPrincipal = Thread.CurrentPrincipal;
    try
    {
        Thread.CurrentPrincipal = new MyPrincipal(...);
        SUT = new MyViewModel();
    }
    finally
    {
        Thread.CurrentPrincipal = previousPrincipal;
    }
}

例如,ReSharper为我们处理所有通信工作。 MSpec 的 ReSharper Runner 可以连接到现有的基础设施(据我所知,它不使用 .NET Remoting)。

Dan,

thank you for providing a reproduction.

First off, the console runner works differently than the TestDriven.NET and ReSharper runners. Basically, the console runner has to perform a lot more setup work in that it creates a new AppDomain (plus configuration) for every assembly that is run. This is required to load the .dll.config file for your spec assembly.

Per spec assembly, two AppDomains are created:

  1. The first AppDomain (Console) is created
    implicitly when mspec.exe is
    executed,
  2. a second AppDomain is created by mspec.exe for the assembly containing the specs (Spec).

Both AppDomains communicate with each other through .NET Remoting: For example, when a spec is executed in the Spec AppDomain, it notifies the Console AppDomain of that fact. When Console receives the notification it acts accordingly by writing the spec information to the console.

This communiciation between Spec and Console is realized transparently through .NET Remoting. One property of .NET Remoting is that some properties of the calling AppDomain (Spec) are automatically included when sending notifications to the target AppDomain (Console). Thread.CurrentPrincipal is such a property. You can read more about that here: http://sontek.vox.com/library/post/re-iprincipal-iidentity-ihttpmodule-serializable.html

The context you provide will run in the Spec AppDomain. You set Thread.CurrentPrincipal in the Because. After Because ran, a notification will be issued to the Console AppDomain. The notification will include your custom MyPrincipal that the receiving Console AppDomain tries to deserialize. It cannot do that since it doesn't know about your spec assembly (as it is not included in its private bin path).

This is why you had to put your spec assembly in the same folder as mspec.exe.

There are two possible workarounds:

  1. Derive MyPrincipal and MyIdentity from MarshalByRefObject so that they can take part in cross-AppDomain communication through a proxy (instead of being serialized)
  2. Set Thread.CurrentPrincipal transiently in the Because

(Text is required for formatting to work -- please ignore)

Because of = () => 
{
    var previousPrincipal = Thread.CurrentPrincipal;
    try
    {
        Thread.CurrentPrincipal = new MyPrincipal(...);
        SUT = new MyViewModel();
    }
    finally
    {
        Thread.CurrentPrincipal = previousPrincipal;
    }
}

ReSharper, for example, handles all the communication work for us. MSpec's ReSharper Runner can hook into the existing infrastructure (that, AFAIK, does not use .NET Remoting).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文