具有隐藏状态的类的单元测试对象创建(查询对象模式)

时间:2013-01-31 17:41:55

标签: c# unit-testing tdd nunit

我有以下代码:

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服务。然后,我可以测试在模拟查询上调用的正确方法。但是,这仍然让我对工厂本身进行了具有挑战性的测试。

  4. 任何有关测试/重新分解此代码以使其更易于测试的建议都将受到赞赏。

2 个答案:

答案 0 :(得分:1)

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

答案 1 :(得分:1)

您似乎有两个职责:从数据源获取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] }));
}