模拟存储库并测试参数化服务方法

时间:2014-04-23 17:39:52

标签: c# unit-testing mocking expression moq

我花了几天寻找解决方案,这允许我模拟由Expression<Func<T, bool>>参数化的方法。我找到了this。但遗憾的是,当我想使用字符串参数测试服务方法时,它不起作用,例如:public IEnumerable<Person> FindByName(string name),如下所示:

using System;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace UnitTestProject
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void TestMethod1()
        {
            var mock = new Mock<IRepository<Person>();

            mock.Setup(r => r.Find(AreEqual<Person>(p => p.FirstName.Equals("Justin")))).Returns(new[]
                {
                    new Person {FirstName = "Justin", LastName = "Smith"},
                    new Person {FirstName = "Justin", LastName = "Quincy"}
                });

            var personService = new PersonService(mock.Object);
            Person[] justins = personService.FindByName("Justin").ToArray();
            Person[] etheredges = personService.FindByName("Etheredge").ToArray();

            Debugger.Break();
        }

        static Expression<Func<T, bool>> AreEqual<T>(Expression<Func<T, bool>> expr)
        {
            return Match.Create<Expression<Func<T, bool>>>(t => t.ToString() == expr.ToString());
        }
    }

    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

    public interface IRepository<T>
    {
        IEnumerable<T> Find(Expression<Func<Person, bool>> predicate);
    }

    public class PersonService
    {
        readonly IRepository<Person> _repository;

        public PersonService(IRepository<Person> repository)
        {
            _repository = repository;
        }

        public IEnumerable<Person> FindByName(string name)
        {
            return _repository.Find(p => p.FirstName.Equals(name));
        }
    }
}

当调试器中断时,我希望数组justins包含上面列出的两个项目,而数组etheredges将不包含任何项目。实际上它们都是空阵列。我怀疑是因为在FindByName方法中,字符串不是直接提供的,而是通过变量name提供的。

你知道如何解决这个问题吗?

2 个答案:

答案 0 :(得分:3)

假设您只想测试服务的Find逻辑(并且您信任LINQ :-)),您可以做的就是编译传入的Expression谓词并执行表达式虚假存储库(即pred => fakePeople.Where(pred.Compile()));):

  [TestMethod]
  public void TestMethod1()
  {
     var mock = new Mock<IRepository<Person>>();

     var fakePeople = new[]
          {
             new Person {FirstName = "Justin", LastName = "Smith"},
             new Person {FirstName = "Justin", LastName = "Quincy"},
             new Person {FirstName = "Joe", LastName = "Bloggs"}
          };

     mock.Setup(r => r.Find(It.IsAny<Expression<Func<Person, bool>>>()))
         .Returns<Expression<Func<Person, bool>>>(
             pred => fakePeople.Where(pred.Compile()));

     var personService = new PersonService(mock.Object);

     var searchForJustins = personService.FindByName("Justin");
     Assert.AreEqual(2, searchForJustins.Count());
     Assert.IsTrue(searchForJustins.Any(_ => _.LastName == "Quincy") 
           && searchForJustins.Any(_ => _.LastName == "Smith"));

     var searchForEtheredges = personService.FindByName("Etheredge");
     Assert.IsFalse(searchForEtheredges.Any());
  }

轻微,但是Repository代码本身没有编译 - 我假设你有一个通用的repo模式:

   public interface IRepository<T>
   {
      IEnumerable<T> Find(Expression<Func<T, bool>> predicate);
   }

   public class PersonService
   {
      readonly IRepository<Person> _repository;

      public PersonService(IRepository<Person> repository)
      {
         _repository = repository;
      }

      public IEnumerable<Person> FindByName(string name)
      {
         return _repository.Find(p => p.FirstName.Equals(name));
      }
   }

答案 1 :(得分:1)

问题是您的设置与表达式参数不匹配。这对于使用文字lambda表达式的测试类来说是一个难题。除了匹配参数并返回Type之外,您无法将一个委托与另一个委托真正匹配。

即使表达式在存储库中执行,服务也拥有表达式,服务测试夹具应测试表达式是否产生正确的结果。通过设置匹配表达式,您基本上构建了精细的变更控制测试。

要正确地测试这一点,您必须将数据放入存储库,让存储库在数据上运行表达式(它本身可以被模拟或在内存中),并声明您获得了预期的数据作为回报。 / p>

**

要考虑的其他事情是,您的服务通过传入文字lambda表达式了解存储库的内部工作。你可以做些什么来制作SOLID,就是将表达式从服务中抽象出来,并将它与存储库实现更紧密地联系起来。如果您决定插入调用WCF服务寻找Person的EnterprisePersonRepository,会发生什么?

我会通过静态属性将表达式添加到存储库实现,并使用您的IOC容器来引用它。因此,当您使用IRepository注册PersonRepository时,您还将注册FindByName表达式。

**

最后的想法是,在非教学环境中,这种超抽象和坚持OO教条可能会弄巧成拙。在您寻找解决此问题的解决方案的几天内,可以编写许多简单且经过良好测试的代码。