我以与Julie Lerman's blog post on EF4 mocks and unit tests类似的方式为我的EF存储库创建了一个假冒伪劣文件。
我的问题是如何才能获得假存储库来处理表之间的关系?
假设我有两个包含客户和订单的表。它们之间存在一对多的关系,因此客户可以拥有多个订单。
我的假存储库将设置如下:
public class FakeMyRepository : IMyRepository
{
public FakeMyRepository()
{
Committed = false;
FillCustomers();
FillOrders();
}
public bool Committed { get; set; }
public System.Data.Objects.IObjectSet<Customer> Customers { get; set; }
public System.Data.Objects.IObjectSet<Order> Orders { get; set; }
public void Commit()
{
Committed = true;
}
private void FillCustomers()
{
var data = new List<Customer>()
{
new Customer() { Id = 1, Name = "Jeff" },
new Customer() { Id = 2, Name = "Brian" }
}
this.Customers = new FakeObjectSet<Customer>(data);
}
private void FillOrders()
{
var data = new List<Order>()
{
new Order() { Id = 1, Customer = 1, Value = 100 }
new Order() { Id = 2, Customer = 2, Value = 200 }
new Order() { Id = 3, Customer = 1, Value = 300 }
new Order() { Id = 4, Customer = 2, Value = 400 }
new Order() { Id = 5, Customer = 1, Value = 500 }
}
this.Orders = new FakeObjectSet<Order>(data);
}
}
如果我的测试是这样的,那就通过了:
[TestMethod]
public void FindUserByIdTest()
{
var repo = new FakeMyRepository();
var target = new CustomerService(repo);
var actual = target.GetCustomerById(1);
Assert.IsNotNull(actual);
Assert.AreEqual<string>("Jeff",actual.Name);
}
但是,如果我想说订单的数量,那么它就失败了
[TestMethod]
public void FindUserByIdWithOrderCount()
{
var repo = new FakeMyRepository();
var target = new CustomerService(repo);
var actual = target.GetCustomerById(1);
Assert.IsNotNull(actual);
Assert.AreEqual<int>(3,actual.Orders.Count());
}
有人能指出我正确的方向吗?
干杯。
答案 0 :(得分:1)
您的虚假存储库必须返回已填写订单导航属性的客户。无论如何,这是典型的情况,单元测试没有意义,因为热切或延迟加载是持久层的漏洞抽象。渴望加载(Include
)仅适用于linq-to-entities,延迟加载完全在测试代码之外。
顺便说一下。关于unit testing and Entity framework的事情。
答案 1 :(得分:1)
当您要求actual.Orders.Count()
时失败的原因是因为actual
返回您之前创建的此对象:
new Customer() { Id = 1, Name = "Jeff" }
具体Customer
对象的null
属性为Orders
,因为您从未设置它。
这样看。就代码而言,存储库只是对象的存储机制。它并不关心存储是否由数据库中的表支持,这些表之间存在关系,或者它是否只是放在某个列表中。重要的是,当您调用repo.GetCustomer(1)
时,它会返回一个ID为1的Customer
对象,并填写所有其他详细信息。在这种情况下,您需要将相关订单实际放在Customer
对象中!
所以你可以这样做:
private void FillData()
{
var customer1 = new Customer() { Id = 1, Name = "Jeff" };
var customer2 = new Customer() { Id = 2, Name = "Brian" };
var order1 = new Order() { Id = 1, Customer = 1, Value = 100 };
var order2 = new Order() { Id = 2, Customer = 2, Value = 200 };
var order3 = new Order() { Id = 3, Customer = 1, Value = 300 };
customer1.Orders = new List<Order> {order1, order3};
customer2.Orders = new List<Order> {order2};
this.Customers = new FakeObjectSet<Customer>(new[] {customer1, customer2});
this.Orders = new FakeObjectSet<Order>(new[] {order1, order2, order3});
}
但是你的全套订单。
所有这些,我强烈建议不要使用这样的手动混凝土嘲笑。考虑使用Moq框架:http://code.google.com/p/moq/它将使您的生活更轻松。
编辑:您可能会发现这些内容可用于为单元测试构建上下文。首先,如果你不知道扩展方法是什么,这只是一个例子:
namespace Foo
{
public static class StringExtensions
{
public static bool IsNullOrEmpty(this string input)
{
return string.IsNullOrEmpty(input);
}
}
}
消费者可以做...
using Foo;
string a = null, b = "hello";
a.IsNullOrEmpty(); // returns true
b.IsNullOrEmpty(); // returns false
语法允许您创建可以称为的方法,就像它们是对象上的实例方法一样,但实际上是在其他地方定义的静态方法。
现在,这就是说。您可以创建一些扩展方法和辅助类来为单元测试构建上下文。举个例子。
public static class UnitTestHelper
{
private static int _nextCustomerId = 0;
private static int _nextOrderId = 0;
public static Customer MockCustomer(string name)
{
if (string.IsNullOrEmpty(name)) throw new ArgumentException("name");
var id = _nextCustomerId;
_nextCustomerId += 1;
return new Customer
{
Id = id,
Name = name,
Orders = new List<Order>()
};
}
public static Customer WithOrder(this Customer customer, int value)
{
if (customer == null) throw new ArgumentNullException("customer");
var order = new Order
{
Id = _nextOrderId,
Customer = customer.Id,
Value = value
};
customer.Orders.Add(order);
_nextOrderId += 1;
return customer;
}
public static Mock<Repository> HavingCustomers(this Mock<Repository> repository,
params Customer[] customers)
{
if (repository == null) throw new ArgumentNullException("repository");
var allOrders = customers.SelectMany(c => c.Orders);
repository.Setup(r => r.Customers)
.Returns(new FakeObjectSet<Customer>(customers));
repository.Setup(r => r.Orders)
.Returns(new FakeObjectSet<Order>(allOrders));
return repository;
}
}
一旦你有了这个,你可以做一些类似......的事情,而不是做很多艰苦的手工创作。
[Test]
public void ShouldReturnAllCustomersWithoutOrders()
{
var john = UnitTestHelper.MockCustomer("John").WithOrder(100).WithOrder(200);
var paul = UnitTestHelper.MockCustomer("Paul");
var george = UnitTestHelper.MockCustomer("George").WithOrder(15);
var ringo = UnitTestHelper.MockCustomer("Ringo");
var mockRepository = new Mock<Repository()
.HavingCustomers(john, paul, george, ringo);
var custServ = new CustomerService(mockRepository.Object);
var customersWithoutOrders = custServ.GetCustomersWithoutOrders();
Assert.That(customersWithoutOrders.Count(), Is.EqualTo(2));
Assert.That(customersWithoutOrders, Has.Member(paul));
Assert.That(customersWithoutOrders, Has.Member(ringo));
}
如果要在多个测试中使用,那么可以将该设置提取到附加SetUpAttribute
的方法中。
当你为单元测试定义上下文时,你会想要尽可能多的灵活性,你不想假设每个单元测试你写的是总是希望相同的两个客户拥有相同的八个订单。但这并不意味着你不能编写一些快速帮助方法或类来使设置更容易,更简洁。
希望有所帮助!
答案 2 :(得分:0)
容易腻。只需在初始化客户时填写订单
private void FillCustomers()
{
var data = new List<Customer>()
{
new Customer
{
Id = 1,
Name = "Jeff",
Orders=new List<Order>(new []
{
new Order() { Id = 1, Customer = 1, Value = 100 }
new Order() { Id = 3, Customer = 1, Value = 300 }
new Order() { Id = 5, Customer = 1, Value = 500 }
}
},
new Customer() { Id = 2, Name = "Brian" }
}
this.Customers = new FakeObjectSet<Customer>(data);
}
现在你的测试应该通过了。