7

Tôi đã đọc hàng chục bài viết về PROs và CON cố gắng giả mạo EF giả trong logic nghiệp vụ. Tôi chưa quyết định phải làm gì - nhưng một điều tôi biết là - tôi phải tách các truy vấn khỏi logic nghiệp vụ. Trong this post tôi thấy rằng Ladislav đã trả lời rằng có 2 cách tốt:Decouple EF truy vấn từ BL - Phương pháp mở rộng VS Class-Per-Query

  1. Hãy để họ được nơi họ đang có và sử dụng phương pháp mở rộng thông thường, truy vấn quan điểm, quan điểm cơ sở dữ liệu ánh xạ hoặc xác định tùy chỉnh các truy vấn để xác định phần tái sử dụng .
  2. Hiển thị mọi truy vấn dưới dạng phương thức trên một số lớp riêng biệt. Phương thức không được hiển thị IQueryable và không được chấp nhận Expression làm tham số = toàn bộ logic truy vấn phải được bao bọc trong phương thức. Nhưng điều này sẽ làm cho lớp học của bạn bao gồm các phương pháp liên quan giống như kho lưu trữ (chỉ có một có thể được chế nhạo hoặc giả mạo). Triển khai này gần với thực hiện được sử dụng với các thủ tục được lưu trữ.
  1. Những phương pháp nào bạn nghĩ là tốt hơn bất kỳ lý do tại sao?
  2. Có các yêu cầu ANY để đặt các truy vấn ở vị trí riêng của họ không? (Có thể mất một số chức năng từ EF hoặc một cái gì đó như thế)
  3. Tôi có phải đóng gói thậm chí các truy vấn đơn giản nhất như:

    using (MyDbContext entities = new MyDbContext) 
    { 
        User user = entities.Users.Find(userId); // ENCAPSULATE THIS ? 
    
        // Some BL Code here 
    } 
    

Trả lời

7

Vì vậy, tôi đoán điểm chính của bạn là testability mã của bạn, isn phải không? Trong trường hợp này, bạn nên bắt đầu bằng cách đếm các trách nhiệm của phương thức mà bạn muốn thử nghiệm và hơn là tái cấu trúc mã của bạn bằng cách sử dụng mẫu trách nhiệm duy nhất.

mã ví dụ của bạn có ít nhất ba nhiệm vụ:

  • Tạo một đối tượng là một trách nhiệm - bối cảnh là một đối tượng. Hơn nữa nó là và đối tượng bạn không muốn sử dụng trong thử nghiệm đơn vị của bạn, do đó bạn phải di chuyển sáng tạo của nó ở nơi khác.
  • Truy vấn thực thi là trách nhiệm. Hơn nữa nó là trách nhiệm bạn muốn tránh trong bài kiểm tra đơn vị của bạn.
  • Làm một số logic kinh doanh là một trách nhiệm

Để đơn giản hóa thử nghiệm, bạn nên cấu trúc lại mã của bạn và chia những trách nhiệm với các phương pháp riêng biệt.

public class MyBLClass() 
{ 
    public void MyBLMethod(int userId) 
    { 
     using (IMyContext entities = GetContext()) 
     { 
      User user = GetUserFromDb(entities, userId); 

      // Some BL Code here 
     } 
    } 

    protected virtual IMyContext GetContext() 
    { 
     return new MyDbContext(); 
    } 

    protected virtual User GetUserFromDb(IMyDbContext entities, int userId) 
    { 
     return entities.Users.Find(userId); 
    } 
} 

Bây giờ kiểm tra đơn vị logic kinh doanh nên miếng bánh vì kiểm tra đơn vị của bạn có thể kế thừa lớp và phương thức factory bối cảnh giả và thực hiện truy vấn phương pháp của bạn và trở thành hoàn toàn độc lập trên EF.

// NUnit unit test 
[TestFixture] 
public class MyBLClassTest : MyBLClass 
{ 
    private class FakeContext : IMyContext 
    { 
     // Create just empty implementation of context interface 
    } 

    private User _testUser; 

    [Test] 
    public void MyBLMethod_DoSomething() 
    { 
     // Test setup 
     int id = 10; 
     _testUser = new User 
      { 
       Id = id, 
       // rest is your expected test data - that is what faking is about 
       // faked method returns simply data your test method expects 
      }; 

     // Execution of method under test 
     MyBLMethod(id); 

     // Test validation 
     // Assert something you expect to happen on _testUser instance 
     // inside MyBLMethod 
    } 

    protected override IMyContext GetContext() 
    { 
     return new FakeContext(); 
    } 

    protected override User GetUserFromDb(IMyContext context, int userId) 
    { 
     return _testUser.Id == userId ? _testUser : null; 
    } 
} 

Khi bạn thêm các phương pháp và ứng dụng của bạn phát triển, bạn sẽ cấu trúc lại các phương pháp thực hiện truy vấn và phương thức factory bối cảnh đến các lớp học riêng biệt để làm theo trách nhiệm duy nhất trên lớp cũng - bạn sẽ nhận được máy bối cảnh và một trong hai số nhà cung cấp truy vấn hoặc trong một số trường hợp kho lưu trữ (nhưng kho lưu trữ đó sẽ không bao giờ trả lại IQueryable hoặc nhận được Expression làm thông số trong bất kỳ phương pháp nào của nó). Điều này cũng sẽ cho phép bạn tuân theo nguyên tắc DRY nơi tạo ngữ cảnh của bạn và các truy vấn được sử dụng phổ biến nhất sẽ chỉ được xác định một lần trên một vị trí trung tâm.

Vì vậy, cuối cùng bạn có thể có một cái gì đó như thế này:

public class MyBLClass() 
{ 
    private IContextFactory _contextFactory; 
    private IUserQueryProvider _userProvider; 

    public MyBLClass(IContextFactory contextFactory, IUserQueryProvider userProvider) 
    { 
     _contextFactory = contextFactory; 
     _userProvider = userProvider; 
    } 

    public void MyBLMethod(int userId) 
    { 
     using (IMyContext entities = _contextFactory.GetContext()) 
     { 
      User user = _userProvider.GetSingle(entities, userId); 

      // Some BL Code here 
     } 
    } 
} 

đâu những giao diện sẽ trông giống như:

public interface IContextFactory 
{ 
    IMyContext GetContext(); 
} 

public class MyContextFactory : IContextFactory 
{ 
    public IMyContext GetContext() 
    { 
     // Here belongs any logic necessary to create context 
     // If you for example want to cache context per HTTP request 
     // you can implement logic here. 
     return new MyDbContext(); 
    } 
} 

public interface IUserQueryProvider 
{ 
    User GetUser(int userId); 

    // Any other reusable queries for user entities 
    // Non of queries returns IQueryable or accepts Expression as parameter 
    // For example: IEnumerable<User> GetActiveUsers(); 
} 

public class MyUserQueryProvider : IUserQueryProvider 
{ 
    public User GetUser(IMyContext context, int userId) 
    { 
     return context.Users.Find(userId); 
    } 

    // Implementation of other queries 

    // Only inside query implementations you can use extension methods on IQueryable 
} 

thử nghiệm của bạn bây giờ sẽ chỉ sử dụng hàng giả cho nhà máy sản xuất ngữ cảnh và nhà cung cấp truy vấn.

// NUnit + Moq unit test 
[TestFixture] 
public class MyBLClassTest 
{ 
    private class FakeContext : IMyContext 
    { 
     // Create just empty implementation of context interface 
    } 

    [Test] 
    public void MyBLMethod_DoSomething() 
    { 
     // Test setup 
     int id = 10; 
     var user = new User 
      { 
       Id = id, 
       // rest is your expected test data - that is what faking is about 
       // faked method returns simply data your test method expects 
      }; 

     var contextFactory = new Mock<IContextFactory>(); 
     contextFactory.Setup(f => f.GetContext()).Returns(new FakeContext()); 

     var queryProvider = new Mock<IUserQueryProvider>(); 
     queryProvider.Setup(f => f.GetUser(It.IsAny<IContextFactory>(), id)).Returns(user); 

     // Execution of method under test 
     var myBLClass = new MyBLClass(contextFactory.Object, queryProvider.Object); 
     myBLClass.MyBLMethod(id); 

     // Test validation 
     // Assert something you expect to happen on user instance 
     // inside MyBLMethod 
    } 
} 

Sẽ có một chút khác biệt trong trường hợp kho lưu trữ phải tham chiếu đến ngữ cảnh được truyền cho nhà xây dựng trước khi đưa nó vào lớp doanh nghiệp của bạn. Lớp doanh nghiệp của bạn vẫn có thể định nghĩa một số truy vấn không bao giờ được sử dụng trong bất kỳ lớp nào khác - những truy vấn đó có lẽ là một phần của logic của nó. Bạn cũng có thể sử dụng các phương thức mở rộng để xác định một số truy vấn có thể dùng lại nhưng bạn phải luôn sử dụng các phương thức mở rộng này bên ngoài logic nghiệp vụ cốt lõi mà bạn muốn kiểm thử đơn vị (trong phương thức thực hiện truy vấn hoặc trong nhà cung cấp truy vấn/kho lưu trữ). Điều đó sẽ cho phép bạn dễ dàng giả mạo truy vấn nhà cung cấp hoặc các phương pháp thực hiện truy vấn.

Tôi thấy your previous question và nghĩ về cách viết bài đăng blog về chủ đề đó nhưng cốt lõi của ý kiến ​​của tôi về thử nghiệm với EF là trong câu trả lời này.

Edit:

Repository là chủ đề khác nhau mà không liên quan đến câu hỏi ban đầu của bạn. Kho lưu trữ cụ thể vẫn là mẫu hợp lệ. Chúng tôi không chống lại kho lưu trữ, we are against generic repositories vì chúng không cung cấp bất kỳ tính năng bổ sung nào và không giải quyết bất kỳ vấn đề nào.

Vấn đề là chỉ kho lưu trữ không giải quyết được gì. Có ba mẫu mà phải được sử dụng với nhau để tạo thành trừu tượng thích hợp: Kho lưu trữ, Đơn vị công việc và Thông số kỹ thuật. Tất cả ba đã có sẵn trong EF: DbSet/ObjectSet như kho, DbContext/ObjectContext là Đơn vị công trình và LINQ to Entities dưới dạng thông số kỹ thuật. Vấn đề chính với việc triển khai tùy chỉnh kho chung được đề cập ở khắp mọi nơi là chúng chỉ thay thế kho lưu trữ và đơn vị làm việc với triển khai tùy chỉnh nhưng vẫn phụ thuộc vào các đặc tả ban đầu => trừu tượng không đầy đủ và nó bị rò rỉ trong các bài kiểm tra nơi kho lưu trữ giả hoạt động theo cùng cách bộ bối cảnh/bối cảnh.

Những bất lợi chính của nhà cung cấp truy vấn của tôi là phương pháp rõ ràng cho bất kỳ truy vấn nào bạn sẽ cần thực thi. Trong trường hợp kho lưu trữ, bạn sẽ không có các phương thức như vậy, bạn sẽ chỉ có vài phương thức chấp nhận đặc tả (nhưng một lần nữa các đặc tả đó phải được định nghĩa theo nguyên tắc DRY) sẽ xây dựng các điều kiện lọc truy vấn, sắp xếp vv

public interface IUserRepository 
{ 
    User Find(int userId); 
    IEnumerable<User> FindAll(ISpecification spec); 
} 

Thảo luận về chủ đề này nằm ngoài phạm vi của câu hỏi này và yêu cầu bạn tự học.

Btw. chế nhạo và giả mạo có mục đích khác nhau - bạn giả mạo một cuộc gọi nếu bạn cần lấy dữ liệu thử nghiệm từ phương thức trong sự phụ thuộc và bạn giả lập cuộc gọi nếu bạn cần xác nhận rằng phương thức phụ thuộc được gọi với các đối số dự kiến.

+0

Tôi rất vui vì bạn là người đã trả lời, vì bạn có vẻ là 'Người đi tìm' về EF Testability. Tôi có một vài câu hỏi: Bạn nói ** bạn sẽ nhận được nhà máy ngữ cảnh và một số nhà cung cấp truy vấn hoặc trong một số trường hợp kho lưu trữ (nhưng kho lưu trữ đó sẽ không bao giờ trả về IQueryable hoặc lấy Expression làm tham số trong bất kỳ phương thức nào của nó). Điều này cũng sẽ cho phép bạn tuân theo nguyên tắc DRY nơi tạo ngữ cảnh của bạn và các truy vấn được sử dụng phổ biến nhất sẽ chỉ được định nghĩa một lần trên một vị trí trung tâm. ** - Tôi nghĩ bạn hoàn toàn chống lại việc sử dụng kho lưu trữ. Bạn có thể làm rõ với một ví dụ? –

+0

Bạn cũng có thể làm rõ câu này: ** 'Sẽ khác đôi chút trong trường hợp kho lưu trữ có tham chiếu đến ngữ cảnh được truyền cho hàm dựng của nó trước khi đưa nó vào lớp nghiệp vụ của bạn.' ** (ví dụ về kho lưu trữ) của loại này và khi người ta sử dụng nó sẽ được đánh giá cao) –

+0

và cuối cùng - khối cuối cùng bạn đã viết: ** Lớp doanh nghiệp của bạn vẫn có thể xác định một số truy vấn không bao giờ sử dụng trong bất kỳ lớp nào khác - những truy vấn đó có thể là một phần của logic của nó. ** - ví dụ mã đầu tiên của bạn sử dụng kỹ thuật này, phải không? khi nào tôi sẽ sử dụng và khi nào tôi sẽ sử dụng các phương pháp mở rộng? nơi nào các phương pháp mở rộng này đi? –

Các vấn đề liên quan