在 C# 中测试 Environment.Exit()

Test Environment.Exit() in C#

C# 中是否有某种等价于 ExpectedSystemExit in Java? I have an exit in my code and would really like to be able to test it. The only thing I found in C# is a not really nice workaround.

的东西?

示例代码

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

测试代码

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

    ???
}

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

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

例如:

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

我们还假设您有一个调用 UserHasRights():

的接口
public interface IRightsService
{
    bool UserHasRights();
}

现在假设您要测试的 class 看起来像这样:

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。)

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

例如,您可以像这样为 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 class:

中消失
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.

如果您的目标是避免额外的 classes/interfaces 只是为了支持测试,您如何看待通过 属性 注入进行的 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);
}

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

代码

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