【问题标题】:Unit Testing Object Creation of Class with Hidden State (Query Object Pattern)具有隐藏状态的类的单元测试对象创建(查询对象模式)
【发布时间】:2013-01-31 17:41:54
【问题描述】:

我有以下代码:

public class Foo
{
    public string Name { get; set; }
}

//query object pattern
public class FooQuery
{
    private string _startsWith;

    public FooQuery NameStartsWith(string startsWith)
    {
        _startsWith = startsWith;
        return this;
    }

    public List<Foo> Execute(IQueryable<Foo> someContext)
    {
        if (!string.IsNullOrWhiteSpace(_startsWith))
            someContext = someContext.Where(f => f.Name.StartsWith(_startsWith));
        return someContext.ToList();
    }
}

public interface IFooService
{
    List<Foo> FindByNameStartsWith(string startsWith);
}

public class FooService : IFooService
{
    private readonly IFooRepository _fooRepository;

    public FooService(IFooRepository fooRepository)
    {
        _fooRepository = fooRepository;
    }

    public List<Foo> FindByNameStartsWith(string startsWith)
    {
        var query = new FooQuery().NameStartsWith(startsWith);
        return _fooRepository.Find(query);
    }
}

public interface IFooRepository
{
    List<Foo> Find(FooQuery query);
}

public class FooRepository : IFooRepository
{
    public List<Foo> Find(FooQuery query)
    {
        var someContext = new List<Foo>().AsQueryable(); //would be EF/Mongo, etc
        return query.Execute(someContext);

    }
}

基本上,我有一个服务“FooService”,它更新一个查询对象“FooQuery”并根据传递给它的方法参数设置它的状态。然后,该服务将查询传递到存储库“FooRepository”,在该存储库中进行数据访问。 FooQuery 故意不通过属性公开其状态。相反,它公开了用于更好控制的方法。我需要对 FooService 已正确创建查询对象进行单元测试。

这是一个挑战,因为 FooQuery 的状态对于单元测试是不可见的。我看到了几个选项,但似乎都闻起来了:

  1. 将 FooQuery 的状态公开为只读属性,并在单元测试中检查这些属性在传递到存储库时是否有效。 (检查查询的状态在技术上可以通过带有回调的模拟框架来实现。)我不喜欢这样,因为我们现在必须为了测试目的而打开状态并修改代码。

  2. 保持代码不变,并测试服务方法生成的结果是否与查询对象的结果相同。我不喜欢这样,因为它使单元测试变得更大、更少结论性和更多冗余(在检查结果时我必须对查询对象本身进行非常相似的测试)

  3. 将 FooQuery 包装在一个接口中并创建一个要注入 foo 服务的工厂。然后我可以测试在模拟查询上调用的正确方法。但是,这仍然给我留下了对工厂本身的挑战性测试。

任何有关测试/重构此代码以使其更易于测试的建议将不胜感激。

【问题讨论】:

    标签: c# unit-testing tdd nunit


    【解决方案1】:

    您可以在FooQuery 上实现Equals,然后验证您是否通过模拟的IFooRepository 获得了预期的FooQuery。这是您的选项#1 的有限形式。

    【讨论】:

      【解决方案2】:

      您似乎有两个职责:从数据源获取 IQueryable,并对其执行查询。也许更简单的设计有意义?

      public interface IFooRepository
      {
           IQueryable<Foo> GetFoo();
      }
      
      public FooService : IFooService
      {
           public List<Foo> FindByNameStartsWith(string startsWith)
           {
                return new FooQuery().StartsWith(startsWith).Execute(_fooRepo.GetFoo());
           }
      }
      

      现在 FooService 易于测试,数据库特定逻辑与所有其他逻辑分离。

      [Test]
      public void StartsWithFiltersFooFromRepository()
      {
          var fooFromRepository = new List<Foo> { new Foo {Name="yes1"}, new Foo {Name="no"}, new Foo {Name="yes2"} };
          _fooRepMock.Setup(r=>r.GetFoo()).Returns(fooFromRepository);
      
          var actual = _fooService.FindByNameStartsWith("yes");
      
          Assert.That(actual, Is.EquivalentTo(new [] { fooFromRepository[0], fooFromRepository[2] }));
      }
      

      【讨论】:

      • 这可行,但我想将我的查询逻辑保留在 FooQuery 类中,因为它在其他服务之间重复使用。
      • 如果你喜欢,你仍然可以。这感觉像是不必要的复杂性,会让你的生活更加艰难,但你最了解你的背景。在这种情况下,测试完全一样,并更改实现以使用 FooQuery。我会进行适当的修改。
      • 谢谢。实际上,我认为查询对象模式简化了而不是使事情变得更复杂,因为您将查询本身的责任转移到了这个类。该服务只是提供查询值并与存储库协调。通过存储库向您的数据源公开 GetAll() 方法 - 一个 IQueryable 可能会在更大的系统中引发问题,因为您可以执行的查询类型没有限制。
      猜你喜欢
      • 2017-12-11
      • 1970-01-01
      • 2011-09-23
      • 1970-01-01
      • 2010-12-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多