您应该使用依赖注入来向被测试的类提供一个提供环境退出的接口。
例如:
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();
}
}
有关环境上下文的更多信息,请参见此处。
IApplicationLifetimeManagement
的接口,并在其中定义一个名为Terminate
的方法。您将把这个接口注入到您要测试的任何内容中。在测试过程中,您需要模拟出该接口的虚拟实现,并将其注入到您要测试的代码中,然后在测试完成后验证是否调用了Terminate
方法。显然,该方法的虚拟实现不会调用Environment.Exit
。您可以使用NSubstitute
或类似的工具来进行模拟。 - Lasse V. Karlsen