您应该使用依赖注入来为被测试的类提供一个提供环境出口的接口。
例如:
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.