2014-11-11 15 views
0

Я создаю приложения в .net/C#/Entity Framework, которая использует многоуровневую архитектуру. Интерфейс приложений для внешнего мира - это уровень сервиса WCF. Под этим слоем у меня есть BL, Shared Library и DAL.Построить тестовую логику бизнес-уровня

Теперь, чтобы сделать бизнес-логику в моем приложении проверяемым, я пытаюсь ввести разделение проблем, ослабление связи и высокую степень сцепления, чтобы иметь возможность вводить зависимости во время тестирования.

Мне нужны некоторые указания относительно того, подходит ли мой подход, описанный ниже, или если я еще раз развяжу код.

Следующий фрагмент кода используется для запроса базы данных с использованием динамического linq. Мне нужно использовать динамический linq, поскольку я не знаю имя таблицы или поля для запроса до выполнения. Код сначала анализирует JSON параметры на типы объектов, а затем строит запрос с помощью этих параметров, и, наконец, запрос выполнения и результат возвращается

Вот функция GetData, которая используется в тесте ниже

IQueryHelper helper = new QueryHelper(Context.DatabaseContext); 

//1. Prepare query 
LinqQueryData queryData = helper.PrepareQueryData(filter); 

//2. Build query 
IQueryable query = helper.BuildQuery(queryData); 

//3. Execute query 
List<dynamic> dalEntities = helper.ExecuteQuery(query); 

Здесь высокий уровень definion хелперного запроса класса в ДАЛ и его интерфейс

public interface IQueryHelper 
{ 
    LinqQueryData PrepareQueryData(IDataQueryFilter filter); 
    IQueryable BuildQuery(LinqQueryData queryData); 
    List<dynamic> ExecuteQuery(IQueryable query); 
} 

public class QueryHelper : IQueryHelper 
{ 
    .. 
    .. 
} 

Вот тест, который использует логику, как описано выше. Тест конструктор впрыскивает высмеивал дб в Context.DatabaseContext

[TestMethod] 
public void Verify_GetBudgetData() 
{ 
    Shared.Poco.User dummyUser = new Shared.Poco.User(); 
    dummyUser.UserName = "dummy"; 

    string groupingsJSON = "[\"1\",\"44\",\"89\"]"; 
    string valueTypeFilterJSON = "{1:1}"; 
    string dimensionFilter = "{2:[\"200\",\"300\"],1:[\"3001\"],44:[\"1\",\"2\"]}"; 

    DataQueryFilter filter = DataFilterHelper.GetDataQueryFilterByJSONData(
    new FilterDataJSON() 
    { 
     DimensionFilter = dimensionFilter, 
     IsReference = false, 
     Groupings = groupingsJSON, 
     ValueType = valueTypeFilterJSON 
    }, dummyUser); 

    FlatBudgetData data = DataAggregation.GetData(dummyUser, filter); 
    Assert.AreEqual(2, data.Data.Count); 
    //min value for january and february 
    Assert.AreEqual(50, Convert.ToDecimal(data.Data.Count > 0 ? data.Data[0].AggregatedValue : -1)); 
} 

На мои вопросы

  1. эта логика бизнес-слой «достаточно хорошо» или, что еще можно сделать для достижения слабосвязанности, высокая когезия и проверяемый код?
  2. Должен ли я вставлять контекст данных в запрос в конструкторе? Обратите внимание, что определения QueryHelper находятся в DAL. Код , который использует его, находится в BL

Пожалуйста, дайте мне знать, если я должен опубликовать дополнительный код для ясности. Меня больше всего интересует, если интерфейс IQueryHelper достаточен.

+0

Является 'DataAggregation.GetData' делает прямой поиск в тестовой базе данных? – beautifulcoder

+0

GetData запускает код, как описано в 3 шагах. Context.DatabaseContext переопределяется путем издевательских данных путем ввода кода. Таким образом, в этом случае функция getdata переходит в издеваемую базу данных. Обычно он использует реальную базу данных. – JohanLarsson

ответ

0

Обычно я использую IServices, Services и MockServices.

  • IServices предоставляет доступные операции, в которые должна ссылаться вся бизнес-логика.
  • Службы - это уровень доступа к данным, который мой код-код вводит в модель вида (т. Е. Фактическая база данных).
  • MockServices - это уровень доступа к данным, который мой модуль тестирует, внедряя в модель представления (т. Е. Данные макета).

IServices:

public interface IServices 
{ 
    IEnumerable<Warehouse> LoadSupply(Lookup lookup); 
    IEnumerable<Demand> LoadDemand(IEnumerable<string> stockCodes, int daysFilter, Lookup lookup); 

    IEnumerable<Inventory> LoadParts(int daysFilter); 
    Narration LoadNarration(string stockCode); 
    IEnumerable<PurchaseHistory> LoadPurchaseHistory(string stockCode); 

    IEnumerable<StockAlternative> LoadAlternativeStockCodes(); 
    AdditionalInfo GetSupplier(string stockCode); 
} 

MockServices:

public class MockServices : IServices 
{ 
    #region Constants 
    const int DEFAULT_TIMELINE = 30; 
    #endregion 

    #region Singleton 
    static MockServices _mockServices = null; 

    private MockServices() 
    { 
    } 

    public static MockServices Instance 
    { 
     get 
     { 
      if (_mockServices == null) 
      { 
       _mockServices = new MockServices(); 
      } 

      return _mockServices; 
     } 
    } 
    #endregion 

    #region Members 
    IEnumerable<Warehouse> _supply = null; 
    IEnumerable<Demand> _demand = null; 
    IEnumerable<StockAlternative> _stockAlternatives = null; 
    IConfirmationInteraction _refreshConfirmationDialog = null; 
    IConfirmationInteraction _extendedTimelineConfirmationDialog = null; 
    #endregion 

    #region Boot 
    public MockServices(IEnumerable<Warehouse> supply, IEnumerable<Demand> demand, IEnumerable<StockAlternative> stockAlternatives, IConfirmationInteraction refreshConfirmationDialog, IConfirmationInteraction extendedTimelineConfirmationDialog) 
    { 
     _supply = supply; 
     _demand = demand; 
     _stockAlternatives = stockAlternatives; 
     _refreshConfirmationDialog = refreshConfirmationDialog; 
     _extendedTimelineConfirmationDialog = extendedTimelineConfirmationDialog; 
    } 

    public IEnumerable<StockAlternative> LoadAlternativeStockCodes() 
    { 
     return _stockAlternatives; 
    } 

    public IEnumerable<Warehouse> LoadSupply(Lookup lookup) 
    { 
     return _supply; 
    } 

    public IEnumerable<Demand> LoadDemand(IEnumerable<string> stockCodes, int daysFilter, Syspro.Business.Lookup lookup) 
    { 
     return _demand; 
    } 

    public IEnumerable<Inventory> LoadParts(int daysFilter) 
    { 
     var job1 = new Job() { Id = Globals.jobId1, AssembledRequiredDate = DateTime.Now, StockCode = Globals.stockCode100 }; 
     var job2 = new Job() { Id = Globals.jobId2, AssembledRequiredDate = DateTime.Now, StockCode = Globals.stockCode200 }; 
     var job3 = new Job() { Id = Globals.jobId3, AssembledRequiredDate = DateTime.Now, StockCode = Globals.stockCode300 }; 

     return new HashSet<Inventory>() 
     { 
      new Inventory() { StockCode = Globals.stockCode100, UnitQTYRequired = 1, Category = "Category_1", Details = new PartDetails() { Warehouse = Globals.Instance.warehouse1, Job = job1} }, 
      new Inventory() { StockCode = Globals.stockCode200, UnitQTYRequired = 2, Category = "Category_1", Details = new PartDetails() { Warehouse = Globals.Instance.warehouse1, Job = job2} }, 
      new Inventory() { StockCode = Globals.stockCode300, UnitQTYRequired = 3, Category = "Category_1", Details = new PartDetails() { Warehouse = Globals.Instance.warehouse1, Job = job3} }, 
     }; 
    } 
    #endregion 

    #region Selection 
    public Narration LoadNarration(string stockCode) 
    { 
     return new Narration() 
     { 
      Text = "Some description" 
     }; 
    } 

    public IEnumerable<PurchaseHistory> LoadPurchaseHistory(string stockCode) 
    { 
     return new List<PurchaseHistory>(); 
    } 

    public AdditionalInfo GetSupplier(string stockCode) 
    { 
     return new AdditionalInfo() 
     { 
      SupplierName = "Some supplier name" 
     }; 
    } 
    #endregion 

    #region Creation 
    public Inject Dependencies(IEnumerable<Warehouse> supply, IEnumerable<Demand> demand, IEnumerable<StockAlternative> stockAlternatives, IConfirmationInteraction refreshConfirmation = null, IConfirmationInteraction extendedTimelineConfirmation = null) 
    { 
     return new Inject() 
     { 
      Services = new MockServices(supply, demand, stockAlternatives, refreshConfirmation, extendedTimelineConfirmation), 

      Lookup = new Lookup() 
      { 
       PartKeyToCachedParts = new Dictionary<string, Inventory>(), 
       PartkeyToStockcode = new Dictionary<string, string>(), 
       DaysRemainingToCompletedJobs = new Dictionary<int, HashSet<Job>>(), 
. 
. 
. 

      }, 

      DaysFilterDefault = DEFAULT_TIMELINE, 
      FilterOnShortage = true, 
      PartCache = null 
     }; 
    } 

    public List<StockAlternative> Alternatives() 
    { 
     var stockAlternatives = new List<StockAlternative>() { new StockAlternative() { StockCode = Globals.stockCode100, AlternativeStockcode = Globals.stockCode100Alt1 } }; 
     return stockAlternatives; 
    } 

    public List<Demand> Demand() 
    { 
     var demand = new List<Demand>() 
     { 
      new Demand(){ Job = new Job{ Id = Globals.jobId1, StockCode = Globals.stockCode100, AssembledRequiredDate = DateTime.Now}, StockCode = Globals.stockCode100, RequiredQTY = 1}, 
      new Demand(){ Job = new Job{ Id = Globals.jobId2, StockCode = Globals.stockCode200, AssembledRequiredDate = DateTime.Now}, StockCode = Globals.stockCode200, RequiredQTY = 2}, 
     }; 
     return demand; 
    } 

    public List<Warehouse> Supply() 
    { 
     var supply = new List<Warehouse>() 
     { 
      Globals.Instance.warehouse1, 
      Globals.Instance.warehouse2, 
      Globals.Instance.warehouse3, 
     }; 
     return supply; 
    } 
    #endregion 
} 

Услуги:

public class Services : IServices 
{ 
    #region Singleton 
    static Services services = null; 

    private Services() 
    { 
    } 

    public static Services Instance 
    { 
     get 
     { 
      if (services == null) 
      { 
       services = new Services(); 
      } 

      return services; 
     } 
    } 
    #endregion 

    public IEnumerable<Inventory> LoadParts(int daysFilter) 
    { 
     return InventoryRepository.Instance.Get(daysFilter); 
    } 

    public IEnumerable<Warehouse> LoadSupply(Lookup lookup) 
    { 
     return SupplyRepository.Instance.Get(lookup); 
    } 

    public IEnumerable<StockAlternative> LoadAlternativeStockCodes() 
    { 
     return InventoryRepository.Instance.GetAlternatives(); 
    } 

    public IEnumerable<Demand> LoadDemand(IEnumerable<string> stockCodes, int daysFilter, Lookup lookup) 
    { 
     return DemandRepository.Instance.Get(stockCodes, daysFilter, lookup); 
    } 
. 
. 
. 

Unit Test:

[TestMethod] 
    public void shortage_exists() 
    { 
     // Setup 
     var supply = new List<Warehouse>() { Globals.Instance.warehouse1, Globals.Instance.warehouse2, Globals.Instance.warehouse3 }; 
     Globals.Instance.warehouse1.TotalQty = 1; 
     Globals.Instance.warehouse2.TotalQty = 2; 
     Globals.Instance.warehouse3.TotalQty = 3; 

     var demand = new List<Demand>() 
     { 
      new Demand(){ Job = new Job{ Id = Globals.jobId1, StockCode = Globals.stockCode100, AssembledRequiredDate = DateTime.Now}, StockCode = Globals.stockCode100, RequiredQTY = 1}, 
      new Demand(){ Job = new Job{ Id = Globals.jobId2, StockCode = Globals.stockCode200, AssembledRequiredDate = DateTime.Now}, StockCode = Globals.stockCode200, RequiredQTY = 3}, 
      new Demand(){ Job = new Job{ Id = Globals.jobId3, StockCode = Globals.stockCode300, AssembledRequiredDate = DateTime.Now}, StockCode = Globals.stockCode300, RequiredQTY = 4}, 
     }; 

     var alternatives = _mock.Alternatives(); 
     var dependencies = _mock.Dependencies(supply, demand, alternatives); 

     var viewModel = new MainViewModel(); 
     viewModel.Register(dependencies); 

     // Test 
     viewModel.Load(); 

     AwaitCompletion(viewModel); 

     // Verify 
     var part100IsNotShort = dependencies.PartCache.Where(p => (p.StockCode == Globals.stockCode100) && (!p.HasShortage)).Single() != null; 
     var part200IsShort = dependencies.PartCache.Where(p => (p.StockCode == Globals.stockCode200) && (p.HasShortage)).Single() != null; 
     var part300IsShort = dependencies.PartCache.Where(p => (p.StockCode == Globals.stockCode300) && (p.HasShortage)).Single() != null; 

     Assert.AreEqual(true, part100IsNotShort && 
           part200IsShort && 
           part300IsShort); 
    } 

CodeBehnd:

public MainWindow() 
    { 
     InitializeComponent(); 

     this.Loaded += (s, e) => 
      { 
       this.viewModel = this.DataContext as MainViewModel; 

       var dependencies = GetDependencies(); 
       this.viewModel.Register(dependencies); 
. 
. 
. 

ViewModel:

public MyViewModel() 
    { 
. 
. 
. 
    public void Register(Inject dependencies) 
    { 
     try 
     { 
      this.Injected = dependencies; 

      this.Injected.RefreshConfirmation.RequestConfirmation += (message, caption) => 
       { 
        var result = MessageBox.Show(message, caption, MessageBoxButton.YesNo, MessageBoxImage.Question); 
        return result; 
       }; 

      this.Injected.ExtendTimelineConfirmation.RequestConfirmation += (message, caption) => 
       { 
        var result = MessageBox.Show(message, caption, MessageBoxButton.YesNo, MessageBoxImage.Question); 
        return result; 
       }; 

. 
. 
. 
     } 

     catch (Exception ex) 
     { 
      Debug.WriteLine(ex.GetBaseException().Message); 
     } 
    } 
+0

Хорошо, у вас есть пример того, как это может выглядеть? – JohanLarsson

+0

Я обновил ответ. –

+0

Итак, вы используете один IServices для всего бизнес-уровня или есть много разных интерфейсов бизнес-уровня для разных частей вашего бизнес-уровня? – JohanLarsson