【问题标题】:How can I efficiently unit test this complex query?如何有效地对这个复杂的查询进行单元测试?
【发布时间】:2014-02-27 20:35:13
【问题描述】:

我在处理一个大型应用程序。代码库大多分为各种任务,每个任务通过 DI 接收其依赖项(通常是存储库),例如这个简化的理论类:

public class EmailTasks
{
    public EmailTasks( IUserRepository userRepository )
    {
        UserRepository = userRepository;
    }

    private readonly IUserRepository UserRepository;

    public void SendNoticeEmail( DateTime minDate, DateTime maxDate, etc... )
    {
        var users = GetUsersWithNotices( minDate, maxDate, etc... );

        // send email to each user
    }

    private IEnumerable<User> GetUsersWithNotices( DateTime minDate, DateTime maxDate, etc... )
    {
        return UserRepository.FindAll( u => u.Active && !u.Whatever
                                            && u.JoinDate > minDate && u.JoinDate < maxDate
                                            && u.Notices.Any( n => n.Active && !n.Something
                                                                   && Whatever
                                                                   && etc... ) );
    }
}

我的任务是弄清楚如何对GetUsersWithNotices 进行单元测试。测试需要验证方法只返回符合条件的用户。

我不知道从哪里开始。使用 Moq,我可以验证是否调用了 FindAll 方法:

[TestClass]
public class EmailTasksTest
{
    private Mock<IUserRepository> userRepositoryMock;

    [TestInitialize]
    public void MyTestInitialize()
    {
        userRepositoryMock = new Mock<IUserRepository>();
    }

    [TestMethod]
    public void SendNoticeEmailTest()
    {
        var minDate = DateTime.Today.AddDays( -30 );
        var maxDate = DateTime.Today.AddDays( 30 );
        var user1 = new Mock<User>();
        var user2 = new Mock<User>();

        userRepositoryMock.Setup( r => r.FindAll( It.IsAny<Expression<Func<User, bool>>>() ) )
                          .Returns( new List<User>
                                        {
                                            user1Mock.Object,
                                            user2Mock.Object
                                        }.AsQueryable() )
                          .Verifiable();

        var tasks = new EmailTasks( userRepositoryMock.Object );
        task.SendNoticeEmail( minDate, maxDate, etc... );

        userRepositoryMock.Verify();
    }
}

显然,这并不能测试用户是否符合条件。我针对模拟 UserRepository.FindAll 的结果编写的任何测试都只会验证我模拟的内容。

那么我怎样才能有效地对这个复杂的查询进行单元测试呢?

编辑

我试图将业务逻辑与查询分开:

public class EmailTasks
{
    public EmailTasks( IUserRepository userRepository )
    {
        UserRepository = userRepository;
    }

    private readonly IUserRepository UserRepository;

    public void SendNoticeEmail( DateTime minDate, DateTime maxDate, etc... )
    {
        var users = GetUsersWithNotices( minDate, maxDate, etc... );

        // send email to each user
    }

    private IEnumerable<User> GetUsersWithNotices( DateTime minDate, DateTime maxDate, etc... )
    {
        return UserRepository.FindAll( u => UserIsValidForNotice( u, minDate, maxDate, etc... ) );
    }

    private bool UserIsValidForNotice( User user, DateTime minDate, DateTime maxDate, etc... )
    {
        return user.Active && !user.Whatever
               && u.JoinDate > minDate && u.JoinDate < maxDate
               && u.Notices.Any( n => n.Active && !n.Something
                                 && Whatever
                                 && etc... ) );
    }
}

这会导致 NHibernate 抛出异常:

System.ServiceModel.FaultException`1 was unhandled
  HResult=-2146233087
  Message=Boolean UserIsValidForNotice(User, System.DateTime, System.DateTime)
  Source=Castle.Facilities.WcfIntegration
  StackTrace:
       at System.ServiceModel.Channels.ServiceChannel.ThrowIfFaultUnderstood(Message reply, MessageFault fault, String action, MessageVersion version, FaultConverter faultConverter)
       at System.ServiceModel.Channels.ServiceChannel.HandleReply(ProxyOperationRuntime operation, ProxyRpc& rpc)
       at System.ServiceModel.Channels.ServiceChannel.Call(String action, Boolean oneway, ProxyOperationRuntime operation, Object[] ins, Object[] outs, TimeSpan timeout)
       at System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation)
       at System.ServiceModel.Channels.ServiceChannelProxy.Invoke(IMessage message)
       at Castle.Facilities.WcfIntegration.Proxy.WcfRemotingInterceptor.InvokeRealProxy(RealProxy realProxy, WcfInvocation wcfInvocation)
       at Castle.Facilities.WcfIntegration.Proxy.WcfRemotingInterceptor.<>c__DisplayClass1.<PerformInvocation>b__0(WcfInvocation wcfInvocation)
       at Castle.Facilities.WcfIntegration.Proxy.WcfRemotingInterceptor.ApplyChannelPipeline(Int32 policyIndex, WcfInvocation wcfInvocation, Action`1 action)
       at Castle.Facilities.WcfIntegration.Proxy.WcfRemotingInterceptor.<>c__DisplayClass4.<ApplyChannelPipeline>b__3()
       at Castle.Facilities.WcfIntegration.WcfInvocation.Proceed()
       at Castle.Facilities.WcfIntegration.RefreshChannelPolicy.Apply(WcfInvocation invocation)
       at Castle.Facilities.WcfIntegration.Proxy.WcfRemotingInterceptor.ApplyChannelPipeline(Int32 policyIndex, WcfInvocation wcfInvocation, Action`1 action)
       at Castle.Facilities.WcfIntegration.Proxy.WcfRemotingInterceptor.PerformInvocation(IInvocation invocation, Action`1 action)
       at Castle.Facilities.WcfIntegration.Proxy.WcfRemotingInterceptor.PerformInvocation(IInvocation invocation)
       at Castle.Facilities.WcfIntegration.Async.WcfRemotingAsyncInterceptor.PerformInvocation(IInvocation invocation)
       at Castle.Facilities.WcfIntegration.Proxy.WcfRemotingInterceptor.Intercept(IInvocation invocation)
       at Castle.DynamicProxy.AbstractInvocation.Proceed()
       at Castle.Proxies.IEmailServiceProxy.SendNoticeEmail()
       at [Excised]
       at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
       at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
       at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
       at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
       at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       at System.Threading.ThreadHelper.ThreadStart()
  InnerException: 

我想 NHibernate 转换成 SQL 太复杂了。

【问题讨论】:

  • 就个人而言(因为可能有很多解决方案),我会在您的存储库和域逻辑之间创建一个新层。这一层将封装所有这些查询(仅这些查询就很容易测试)。或者甚至可以直接在您的存储库中添加该查询逻辑。在 EmailTask​​ 中,您将能够抽象此查询逻辑(通过模拟新层)以具有易于测试的操作。因为目前,您的班级并不是“只做一件事”,而这很难测试。
  • 提取查询的任何部分的问题是它会导致 NHibernate 抛出异常。我将编辑我的问题以反映我刚刚尝试过的内容。

标签: c# unit-testing nhibernate castle-windsor moq


【解决方案1】:

[编辑] 我做了一些额外的研究,最后问了同样的问题并得到了一些很好的答案: How can I stub an interface method using Moq

这可能是使用存根代替 Moq 的好地方,如下所示:

public class StubRepo : IUserRepository
{
    public IList<User> PersonList { get; set; }

    public IList<User> FindAll(Func<User, bool> q)
    {
        return PersonList.Where(q).ToList();
    }
}

然后,您可以传入一个虚拟人员列表,并验证返回的人员是否满足您的条件。由于 GetUsersWithValidNotices 是私有的,因此您将使用 Moq 来验证您的电子邮件发送逻辑是否仅被调用一次。它看起来像这样:

[TestMethod]
public void TestMethod1()
    {
        //Arrange
        var userList = new List<User>();
        userList .Add(new User { Name="Mike", Active = false });
        userList .Add(new User { Name="Mary", Active = true });
        var stubRepo = new StubRepo{ PersonList = userList});

        var emailSender = Mock<IEmailSender>();

         var emailTask = new EmailTask(stubRepo);
        emailTask.EmailSender = emailSender.Object;


        //Action
        emailTask.SendNoticeEmail(.....);

        //Assert - Verify email only sent to the one active user
        emailSender.Verify(x => x.SendEmail(It.IsAny<User>()), Times.Once())

    }

这将测试您的查询逻辑是否正确,但是,一个巨大的警告是您的查询中的特定函数可能无法转换为 SQL 查询,因此您的查询将在虚拟列表上运行但在针对实际数据库运行时可能会引发异常。单元测试肯定是有价值的,以确保您的所有条件都正确,但您绝对需要针对真实数据库进行集成测试。

[编辑] 我会回到下面显示的原始方法设置。虽然这个查询很大,但它不应该给您带来任何问题,因为您的所有测试都是相对简单的布尔运算。然后你可以按照这篇文章测试整个事情:How can I stub an interface method using Moq。根据我的经验,只要您的 LINQ 查询可以编译成 SQL,那么您使用常规列表对 LINQ 查询执行的任何逻辑单元测试也应该适用于 SQL 查询。

private IEnumerable<User> GetUsersWithNotices( DateTime minDate, DateTime maxDate, etc... )
{
    return UserRepository.FindAll( u => u.Active && !u.Whatever
                                        && u.JoinDate > minDate && u.JoinDate < maxDate
                                        && u.Notices.Any( n => n.Active && !n.Something
                                                               && Whatever
                                                               && etc... ) );
}

【讨论】:

  • “一个巨大的警告是查询中的特定函数可能无法转换为 SQL 查询......”这正是我遇到的问题。我无法分离逻辑来测试它,因为它让 NHibernate 太混乱了。
猜你喜欢
  • 1970-01-01
  • 2011-05-15
  • 2018-06-09
  • 2016-09-26
  • 1970-01-01
  • 2022-11-12
  • 1970-01-01
  • 2016-05-05
  • 2018-03-30
相关资源
最近更新 更多