如何有效地对这个复杂查询进行单元测试?

时间:2014-02-27 20:35:14

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

我在一个大型应用程序上工作。代码库主要分为各种任务,每个任务通过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太复杂了。

1 个答案:

答案 0 :(得分: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... ) );
}