在C#中测试Environment.Exit()函数

8
在C#中是否有类似于Java中的ExpectedSystemExit的等效方法?我在我的代码中有一个退出,希望能够测试它。我在C#中找到的唯一方法是一个不太好的解决方法

示例代码

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

测试代码

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

    ???
}

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


1
你使用了哪个单元测试框架? - Gimly
3
您是否遇到过 Environment.Exit() 无法正常工作的情况?它应该是可以正常工作的。 - csa
1
我正在使用VS框架(+ NSubstitute进行模拟),但是切换到nunit或其他测试框架也不是问题。当然,退出功能是正常的,我需要测试在某些条件下是否被调用。@LasseV.Karlsen,我真的不明白你的意思。当应用程序退出时,即使测试没有执行到最后,它们也会停止执行。 - Antiohia
2
我的意思是,您将创建一个名为IApplicationLifetimeManagement的接口,并在其中定义一个名为Terminate的方法。您将把这个接口注入到您要测试的任何内容中。在测试过程中,您需要模拟出该接口的虚拟实现,并将其注入到您要测试的代码中,然后在测试完成后验证是否调用了Terminate方法。显然,该方法的虚拟实现不会调用Environment.Exit。您可以使用NSubstitute或类似的工具来进行模拟。 - Lasse V. Karlsen
1
显然,我选择的名称只是示例。 - Lasse V. Karlsen
显示剩余3条评论
3个回答

12

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

例如:

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

现在,您可以使用一个mocking框架来检查在正确条件下是否调用了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() 。通过这种修改,RightsChecker 类中的 IEnvironment 参数将消失:

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

有关环境上下文的更多信息,请参见此处。


谢谢...但那是我不喜欢的同样解决方法。难道真的没有类似于JUnit规则的等效物,可以在我的测试中使用,而无需在我的生产代码中创建额外的类/接口/方法吗? - Antiohia
1
@Antiohia,我将在这个答案中补充一下解决横切关注点的常见方法,这至少意味着您不需要传递额外的接口。在C#中,您无法做到这种事情而不创建其他类,但对于真正的横切关注点(如安全、环境控制和当前日期时间),您可以通过引入环境上下文来最小化影响。 - Matthew Watson
谢谢!我会考虑的。 - Antiohia

3
我最终创建了一个新的方法,然后在我的测试中模拟该方法。 代码
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();
}

0

如果您的目标是避免为了支持测试而创建额外的类/接口,那么您对通过属性注入实现 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);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接