修改 Autofac 范围以支持 XUnit 测试

Modifying Autofac Scope to Support XUnit Testing

我广泛使用 Autofac。最近我对在为 XUnit 测试注册项目时调整生命周期范围很感兴趣。基本上我想注册一些我用作“每个测试实例”的标准组件,而不是我通常在运行时做的事情(我在 github 上找到了一个有用的库,它定义了每个测试实例的生命周期) .

一种方法是定义两个单独的容器构建,一个用于运行时,一个用于 xunit 测试。那会奏效,但维护成本越来越高。

我想做的(我认为)是根据构建它的上下文(运行时或 xunit 测试)动态修改注册管道。在伪代码中:

builder.RegisterType<SomeType>().AsImplementedInterfaces().SingleInstance();
...
void TweakPipeline(...)
{
    if( Testing )
    {
        TypeBeingRegistered.InstancePerTest();
    }
    else
    {
        TypeBeingRegistered.SingleInstance();
    }
}

这是Autofac中间件能做的吗?如果不是,Autofac API 中是否有其他功能可以解决它?一如既往,我们将不胜感激。

这是一个有趣的问题。我喜欢您开始考虑 Autofac 中的一些新功能,很少有人这样做。所以,为这个好问题点赞。

如果你考虑中间件,是的,你可能会用它来处理生命周期范围,但我们并没有真正让“动态改变生命周期范围”变得容易,而且......我老实说,我不确定你会怎么做。

但是,我认为有几个不同的选择可以让生活更轻松。如果是我的话,我会按顺序...

选项 1:每个测试的容器

实际上是我为测试所做的。我不会在多个测试中共享一个容器,我实际上将构建容器作为测试设置的一部分。对于 Xunit,这意味着 I put it in the constructor of the test class.

为什么?几个原因:

  • 状态是个问题。我不希望容器中单例的测试顺序或状态使我的测试变得脆弱。
  • 我想测试我部署的东西。我不想测试一些东西 OK 只是发现它有效,因为我在容器中设置了一些特殊的东西测试。明显的模拟异常和使测试实际上成为单元测试的东西。

如果问题是容器的设置时间太长并且正在减慢测试速度,我可能会解决 that。我通常发现造成这种情况的原因要么是我的程序集扫描和注册方式太多了(哎呀,忘记了 Where 语句来过滤东西)要么我已经开始尝试“多用途” “容器通过注册代码在容器构建时自动执行来开始编排我的应用程序启动逻辑(这很容易做到......但不要忘记容器不是你的应用程序启动逻辑,所以也许将它分开) .

每个测试的容器确实是最简单、最独立的方法,不需要特别的努力。

选项 2:模块

Modules 是封装注册集的好方法,也是获取此类参数的好方法。在这种情况下,我可能会为模块做这样的事情:

public class MyModule : Module
{
  public bool Testing { get; set; }

  protected override void Load(ContainerBuilder builder)
  {
    var toUpdate = new List<IRegistrationBuilder<object, ConcreteReflectionActivatorData, SingleRegistrationStyle>>();
    toUpdate.Add(builder.RegisterType<SomeType>());
    toUpdate.Add(builder.RegisterType<OtherType>());

    foreach(var reg in toUpdate)
    {
      if(this.Testing)
      {
        reg.InstancePerTest();
      }
      else
      {
        reg.SingleInstance();
      }
    }
  }
}

那你就可以注册了:

var module = new MyModule { Testing = true };
builder.RegisterModule(module);

这使得注册列表更容易调整(foreach 循环),并且将“需要根据测试更改的内容”隔离到一个模块。

当然,如果你有 lambda 和其他各种注册,它可能会变得有点复杂,但这就是要点。

选项 3:生成器属性

ContainerBuilder 有一组属性,您可以在构建内容时使用它们来帮助避免必须处理环境变量,而且还可以在设置容器时使用任意信息。你可以像这样写一个扩展方法:

public static IRegistrationBuilder<TLimit, TActivatorData, TRegistrationStyle>
  EnableTesting<TLimit, TActivatorData, TRegistrationStyle>(
    this IRegistrationBuilder<TLimit, TActivatorData, TRegistrationStyle> registration,
    ContainerBuilder builder)
  {
    if(builder.Properties.TryGetValue("testing", out var testing) && Convert.ToBoolean(testing))
    {
      registration.InstancePerTest();
    }

    return registration;
  }

然后当你注册需要调整的东西时,你可以这样做:

var builder = new ContainerBuilder();
// Set this in your tests, not in production
// builder.Properties["testing"] = true;
builder.RegisterType<Handler>()
  .SingleInstance()
  .EnableTesting(builder);
var container = builder.Build();

您也许可以稍微清理一下,但同样,这是一般的想法。

您可能会问,如果无论如何都必须传入构建器,为什么还要使用构建器作为传输属性的机制。

  • 语法流畅:由于注册的工作方式,它们都是注册上的扩展方法,而不是构建器上的。注册是一个自包含的东西,没有对构建器的引用(你可以创建一个完全没有构建器的注册对象)。
  • 内部回调:关于注册工作原理的内部机制基本上归结为执行 Action 列表,其中注册在闭包中设置了所有变量。这不是一个我们可以在构建过程中传递东西的函数;它是独立的。 (这可能会很有趣,现在我想到了,但那是另一个讨论!)
  • 您可以将其隔离:您可以将其放入模块或其他任何地方,并且不会添加任何新的依赖项或逻辑。围绕变量的东西将是构建器本身,它始终存在。

正如我所说,您可以根据自己的需要改进它。

建议:每次测试容器

我将通过再次推荐每次测试的容器来结束。非常简单,不需要额外的工作,没有惊喜,而且“就是管用”。