【问题标题】:Test Environment.Exit() in C#在 C# 中测试 Environment.Exit()
【发布时间】:2016-05-20 07:44:37
【问题描述】:

在 C# 中是否存在与 Java 中的 ExpectedSystemExit 等价的东西?我的代码中有一个出口,并且非常希望能够对其进行测试。我在 C# 中发现的唯一一个不是很好的 workaround

示例代码

public void CheckRights()
{
    if(!service.UserHasRights())
    {
         Environment.Exit(1);
    }
}

测试代码

[TestMethod]
public void TestCheckRightsWithoutRights()
{
    MyService service = ...
    service.UserHasRights().Returns(false);

    ???
}

我正在使用 VS 框架进行测试(+ NSubstitute 用于模拟),但切换到 nunit 或其他任何东西进行测试都不是问题。

【问题讨论】:

  • 你使用的是哪个单元测试框架?
  • Heva 您曾经遇到过 Environment.Exit() 不起作用吗?它的工作原理。
  • 我正在使用 VS 框架(+ NSubstitute 用于模拟),但切换到 nunit 或其他任何东西进行测试都不是问题。当然,出口有效,我需要测试它是否在某些条件下被调用。 @LasseV.Karlsen,我不太明白你的意思。当应用程序退出时,它们甚至测试都没有执行到最后。
  • 我的意思是,您将使用Terminate 方法创建一个接口IApplicationLifetimeManagement。您可以将此接口注入到您正在测试的任何内容中。作为测试的一部分,您将模拟该接口的一个虚拟对象,将该虚拟对象注入您正在测试的代码中,然后验证是否调用了 Terminate 方法。显然,该方法的虚拟实现不会调用Environment.Exit。你可以使用NSubstitute 或类似的东西来做这个模拟。
  • 很明显,我选择的名字只是例子。

标签: c# unit-testing integration-testing exit-code


【解决方案1】:

您应该使用依赖注入来为被测试的类提供一个提供环境出口的接口。

例如:

public interface IEnvironment
{
    void Exit(int code);
}

我们还假设你有一个调用UserHasRights()的接口:

public interface IRightsService
{
    bool UserHasRights();
}

现在假设要测试的类如下所示:

public sealed class RightsChecker
{
    readonly IRightsService service;
    readonly IEnvironment environment;

    public RightsChecker(IRightsService service, IEnvironment environment)
    {
        this.service     = service;
        this.environment = environment;
    }

    public void CheckRights()
    {
        if (!service.UserHasRights())
        {
            environment.Exit(1);
        }
    }
}

现在您可以使用模拟框架来检查是否在正确的条件下调用了 IEnvironment .Exit()。例如,使用Moq 可能看起来有点像这样:

[TestMethod]
public static void CheckRights_exits_program_when_user_has_no_rights()
{
    var rightsService = new Mock<IRightsService>();
    rightsService.Setup(foo => foo.UserHasRights()).Returns(false);

    var enviromnent = new Mock<IEnvironment>();

    var rightsChecker = new RightsChecker(rightsService.Object, enviromnent.Object);

    rightsChecker.CheckRights();

    enviromnent.Verify(foo => foo.Exit(1));
}

环境上下文和跨领域关注点

Environment.Exit() 这样的方法可以被认为是一个横切关注点,您可能希望避免为它传递接口,因为您最终可能会遇到额外的构造函数参数的爆炸式增长。 (注意:横切关注点的典型示例是DateTime.Now。)

要解决此问题,您可以引入“环境上下文” - 一种允许您使用静态方法同时仍保留对它的调用进行单元测试的能力的模式。当然,这些东西应该谨慎使用,并且只用于真正的横切关注点。

例如,您可以像这样为Environment 引入环境上下文:

public abstract class EnvironmentControl
{
    public static EnvironmentControl Current
    {
        get
        {
            return _current;
        }

        set
        {
            if (value == null)
                throw new ArgumentNullException(nameof(value));

            _current = value;
        }
    }

    public abstract void Exit(int value);

    public static void ResetToDefault()
    {
        _current = DefaultEnvironmentControl.Instance;
    }

    static EnvironmentControl _current = DefaultEnvironmentControl.Instance;
}

public class DefaultEnvironmentControl : EnvironmentControl
{
    public override void Exit(int value)
    {
        Environment.Exit(value);
    }

    public static DefaultEnvironmentControl Instance => _instance.Value;

    static readonly Lazy<DefaultEnvironmentControl> _instance = new Lazy<DefaultEnvironmentControl>(() => new DefaultEnvironmentControl());
}

普通代码只调用EnvironmentControl.Current.Exit()。通过此更改,IEnvironment 参数从 RightsChecker 类中消失:

public sealed class RightsChecker
{
    readonly IRightsService service;

    public RightsChecker(IRightsService service)
    {
        this.service = service;
    }

    public void CheckRights()
    {
        if (!service.UserHasRights())
        {
            EnvironmentControl.Current.Exit(1);
        }
    }
}

但是我们仍然保留了对它被调用的单元测试的能力:

public static void CheckRights_exits_program_when_user_has_no_rights()
{
    var rightsService = new Mock<IRightsService>();
    rightsService.Setup(foo => foo.UserHasRights()).Returns(false);

    var enviromnent = new Mock<EnvironmentControl>();
    EnvironmentControl.Current = enviromnent.Object;

    try
    {
        var rightsChecker = new RightsChecker(rightsService.Object);
        rightsChecker.CheckRights();
        enviromnent.Verify(foo => foo.Exit(1));
    }

    finally
    {
        EnvironmentControl.ResetToDefault();
    }
}

For more information about ambient contexts, see here.

【讨论】:

  • 谢谢...但这是我不喜欢的相同解决方法。是否真的没有等效的 JUnit 规则可以在我的测试中使用,而无需在我的生产代码中创建额外的类/接口/方法?
  • @Antiohia 我将在此答案中添加针对诸如此类的横切关注点的通用解决方案,这至少意味着您不需要传递其他接口。如果不在 C# 中创建额外的类,你就不能做这种事情,但是对于真正的横切关注点(例如安全、环境控制和当前日期时间),你可以通过引入环境上下文来最小化影响。
  • 谢谢!我会考虑的。
【解决方案2】:

我最终创建了一个新方法,然后我可以在我的测试中进行模拟。

代码

public void CheckRights()
{
    if(!service.UserHasRights())
    {
         Environment.Exit(1);
    }
}

internal virtual void Exit() 
{
    Environment.Exit(1);
}

单元测试

[TestMethod]
public void TestCheckRightsWithoutRights()
{
    MyService service = ...
    service.When(svc => svc.Exit()).DoNotCallBase();
    ...
    service.CheckRights();
    service.Received(1).Exit();
}

【讨论】:

    【解决方案3】:

    如果您的目标是避免额外的类/接口只是为了支持测试,您对 Environment.Exit 通过属性注入操作有何看法?

    class RightsChecker
    {
        public Action AccessDeniedAction { get; set; }
    
        public RightsChecker(...)
        {
            ...
            AccessDeniedAction = () => Environment.Exit();
        }
    }
    
    [Test]
    public TestCheckRightsWithoutRights()
    {
        ...
        bool wasAccessDeniedActionExecuted = false;
        rightsChecker.AccessDeniedAction = () => { wasAccessDeniedActionExecuted = true; }
        ...
        Assert.That(wasAccessDeniedActionExecuted , Is.True);
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-08-27
      • 2012-08-27
      • 2010-11-21
      相关资源
      最近更新 更多