如何在xUnit中实现无参数复用测试,适配多依赖实现?
多依赖实现复用测试用例的简洁方案
现有实现方式
常规参数化写法
这是最基础的实现,通过InlineData传入类型枚举,在测试方法内创建实例:
[Theory] [InlineData(DoerType.Version1)] [InlineData(DoerType.Version2)] public void DoSomethingTest(DoerType type) { IDoSomething doer = Factory.Create(type); string result = doer.DoSomething(); }
自定义Attribute优化版
将实例创建逻辑封装到自定义DataAttribute中,测试方法直接接收实例参数:
[Theory] [DoerVersions(DoerType.Version1, DoerType.Version2)] public void DoSomethingTest(IDoSomething doer) { string result = doer.DoSomething(); } public class DoerVersionsAttribute : DataAttribute { private readonly DoerType[] doers; public DoerVersionsAttribute(params DoerType[] doers) { this.doers = doers; } public override IEnumerable<object[]> GetData(MethodInfo testMethod) { return doers.Select(d => new object[] { Factory.Create(d) }); } }
更符合DRY原则的极致简洁方案
针对大量测试需要复用该逻辑的场景,以下几种方案可以完全消除测试方法参数,让测试代码聚焦业务逻辑:
方案1:测试基类+字段注入
抽象出包含依赖字段的基类,配合自定义Attribute完成实例注入,统一复用逻辑:
// 基类封装依赖字段与注入逻辑 public abstract class DoerTestBase { protected IDoSomething Doer { get; private set; } protected void InjectDoer(IDoSomething doer) { Doer = doer; } } // 自定义Attribute保持实例生成逻辑 public class DoerVersionsAttribute : DataAttribute { private readonly DoerType[] _doers; public DoerVersionsAttribute(params DoerType[] doers) { _doers = doers; } public override IEnumerable<object[]> GetData(MethodInfo testMethod) { return _doers.Select(d => new object[] { Factory.Create(d) }); } } // 测试类继承基类,注入后直接使用字段 public class Tests : DoerTestBase { [Theory] [DoerVersions(DoerType.Version1, DoerType.Version2)] public void DoSomethingTest(IDoSomething doer) { InjectDoer(doer); string result = Doer.DoSomething(); } }
方案2:DI容器+Fixture注入(推荐)
结合测试框架的依赖注入能力,通过IClassFixture完成实例初始化与字段注入,完全消除测试方法参数:
// Fixture封装DI配置与实例创建 public class DoerFixture : IDisposable { public IDoSomething Doer { get; } public DoerFixture(DoerType doerType) { Doer = Factory.Create(doerType); } public void Dispose() { // 清理实例资源 (Doer as IDisposable)?.Dispose(); } } // 自定义Attribute生成Fixture参数 public class DoerVersionsAttribute : DataAttribute { private readonly DoerType[] _doers; public DoerVersionsAttribute(params DoerType[] doers) { _doers = doers; } public override IEnumerable<object[]> GetData(MethodInfo testMethod) { return _doers.Select(d => new object[] { new DoerFixture(d) }); } } // 测试类通过构造函数获取Fixture中的实例 public class Tests : IClassFixture<DoerFixture> { private readonly IDoSomething _doer; public Tests(DoerFixture fixture) { _doer = fixture.Doer; } [Theory] [DoerVersions(DoerType.Version1, DoerType.Version2)] public void DoSomethingTest() { string result = _doer.DoSomething(); } }
方案3:UI测试场景适配
针对你补充的跨接口UI测试需求,可以直接复用上述DI+Fixture方案,将依赖替换为DSL入口:
// Fixture根据接口类型初始化对应DSL public class DslFixture : IDisposable { public IDsl Dsl { get; } public DslFixture(Interface interfaceType) { Dsl = interfaceType switch { Interface.WebChrome => new ChromeDsl(), Interface.WebFirefox => new FirefoxDsl(), Interface.Android => new AndroidDsl(), _ => throw new ArgumentOutOfRangeException() }; } public void Dispose() { Dsl?.Dispose(); } } // 自定义Attribute传入接口类型 public class InterfacesAttribute : DataAttribute { private readonly Interface[] _interfaces; public InterfacesAttribute(params Interface[] interfaces) { _interfaces = interfaces; } public override IEnumerable<object[]> GetData(MethodInfo testMethod) { return _interfaces.Select(i => new object[] { new DslFixture(i) }); } } // 测试类完全聚焦业务逻辑 public class BookSearchTests : IClassFixture<DslFixture> { private readonly IDsl _dsl; public BookSearchTests(DslFixture fixture) { _dsl = fixture.Dsl; } [Theory] [Interfaces(Interface.WebChrome, Interface.WebFirefox, Interface.Android)] public void ShouldBeAbleToSearchBook() { _dsl.Users.Login("user1"); _dsl.Searcher.Search("title:BDD"); _dsl.Searcher.ShouldHaveResult("Some BDD book"); } }
内容的提问来源于stack exchange,提问作者Francesc Castells
相关产品推荐
相关产品推荐

