EF4 - функциональность навигации в фальшивом хранилище? - PullRequest
0 голосов
/ 23 августа 2011

Я создал фальшивку для своего репозитория EF способом, аналогичным описанному в сообщении Джулии Лерман в блоге об издевательствах и модульных тестах EF4 .

У меня вопрос, как я могу получить поддельное хранилище для обработки отношений между таблицами?

Скажем, у меня есть две таблицы с клиентами и заказами. Между ними существует отношение 1 ко многим, поэтому у клиента может быть несколько заказов.

Мой поддельный репозиторий будет настроен примерно так:

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());
}

Кто-нибудь может указать мне правильное направление для этого?

Приветствие.

Ответы [ 3 ]

1 голос
/ 23 августа 2011

Причина сбоя при запросе actual.Orders.Count() состоит в том, что actual возвращает этот объект, который вы создали ранее:

new Customer() { Id = 1, Name = "Jeff" }

И этот конкретный Customer объект имеет null для своего свойства Orders, потому что вы никогда не устанавливали его.

Посмотри на это так. Что касается вашего кода, хранилище - это просто механизм хранения объектов. Не имеет значения, поддерживается ли хранилище таблицами в базе данных со связями между этими таблицами, или же оно просто где-то помещается в список. Важно то, что когда вы вызываете repo.GetCustomer(1), он возвращает вам объект Customer с идентификатором 1 и все остальные данные, которые должны быть заполнены, как и должно быть. В этом случае вам необходимо, чтобы соответствующие ордера действительно находились в объекте 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, если он будет использоваться в нескольких тестах.

Вам понадобится как можно больше гибкости при определении контекста для ваших модульных тестов, вы не хотите предполагать, что для каждого модульного теста вы пишете вам всегда хотят двух одинаковых клиентов с одинаковыми восемью заказами. Но это не значит, что вы не можете написать несколько быстрых вспомогательных методов или классов, чтобы упростить настройку и сделать ее гораздо менее многословной.

Надеюсь, это поможет!

1 голос
/ 23 августа 2011

Ваш поддельный репозиторий должен возвращать клиентов с заполненным свойством навигации по заказам. В любом случае, это типичный сценарий, который не имеет смысла для модульного тестирования, потому что либо энергичная, либо ленивая загрузка - это утечка абстракции вашего уровня персистентности. Стремительная загрузка (Include) работает только с linq-to-entity, а отложенная загрузка происходит полностью вне тестируемого кода.

Btw. кое-что о модульном тестировании и Entity Framework .

0 голосов
/ 23 августа 2011

Легко, peasy. Просто заполняйте заказы при инициализации клиента

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); 
  } 

А теперь ваш тест должен пройти.

...