2011-09-02 22 views
6

Tôi quyết định bắt đầu viết các bài kiểm tra đơn vị trong ứng dụng của chúng tôi. Nó sử dụng Entity Framework với một mẫu kho lưu trữ.Kiểm tra đơn vị Mẫu kho lưu trữ EF với Moq

Bây giờ tôi muốn bắt đầu thử nghiệm các lớp logic đang sử dụng các kho lưu trữ. Tôi cung cấp một ví dụ đơn giản ở đây.

Ba trong số các phương pháp của tôi trong GenericRepository lớp:

public class GenericRepository : IRepository 
{ 
    public IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class 
    { 
     var entityName = GetEntityName<TEntity>(); 
     return Context.CreateQuery<TEntity>(entityName); 
    } 
    private string GetEntityName<TEntity>() where TEntity : class 
    { 
     return typeof(TEntity).Name; 
    } 
    public IEnumerable<TEntity> Find<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class 
    { 
     return GetQuery<TEntity>().Where(predicate).AsEnumerable(); 
    } 
} 

Một lớp logic đơn giản trở về năm khác biệt với một bảng lịch trong thứ tự giảm dần (có tôi biết từ lịch được sai chính tả trong mã của chúng tôi):

public class GetDistinctYearsFromCalendar 
{ 
    private readonly IRepository _repository; 

    public GetDistinctYearsFromCalendar() 
    { 
     _repository = new GenericRepository(); 
    } 

    internal GetDistinctYearsFromCalendar(IRepository repository) 
    { 
     _repository = repository; 
    } 

    public int[] Get() 
    { 
     return _repository.Find<Calender_Tbl>(c => c.Year.HasValue).Select(c => c.Year.Value).Distinct().OrderBy(c => c).Reverse().ToArray(); 
    } 
} 

Và đây là thử nghiệm đầu tiên của tôi:

[TestFixture] 
public class GetDistinctYearsFromCalendarTest 
{ 
    [Test] 
    public void ReturnsDistinctDatesInCorrectOrder() 
    { 
     var repositoryMock = new Mock<IRepository>(); 

     repositoryMock.Setup(r => r.Find<Calender_Tbl>(c => c.Year.HasValue)).Returns(new List<Calender_Tbl> 
     { 
      new Calender_Tbl 
       { 
        Date = 
         new DateTime(2010, 1, 1), 
        Year = 2010 
       }, 
      new Calender_Tbl 
       { 
        Date = 
         new DateTime(2010, 2, 1), 
        Year = 2010 
       }, 
      new Calender_Tbl 
       { 
        Date = 
         new DateTime(2011, 1, 1), 
        Year = 2011 
       } 
     }.AsQueryable()); 

     var getDistinct = new GetDistinctYearsFromCalendar(repositoryMock.Object).Get(); 

     Assert.AreEqual(2, getDistinct.Count(), "Returns more years than distinct."); 
     Assert.AreEqual(2011, getDistinct[0], "Incorrect order, latest years not first."); 
     Assert.AreEqual(2010, getDistinct[1], "Wrong year."); 


    } 
} 

này đang làm việc tốt. Nhưng đây không phải là những gì tôi muốn làm. Vì tôi phải thiết lập phương thức Find trên đối tượng giả, tôi cũng cần phải biết nó sẽ được gọi như thế nào trong lớp logic của tôi. Nếu tôi muốn làm TDD, tôi không muốn bận tâm về điều này. Tất cả những gì tôi muốn biết là các thực thể Lịch mà kho lưu trữ của tôi sẽ cung cấp. Tôi muốn thiết lập phương thức GetQuery. Như thế này:

repositoryMock.Setup(r => r.GetQuery<Calender_Tbl>()).Returns(new List<Calender_Tbl> 
{ 
    new Calender_Tbl 
     { 
      Date = 
       new DateTime(2010, 1, 1), 
      Year = 2010 
     }, 
    new Calender_Tbl 
     { 
      Date = 
       new DateTime(2010, 2, 1), 
      Year = 2010 
     }, 
    new Calender_Tbl 
     { 
      Date = 
       new DateTime(2011, 1, 1), 
      Year = 2011 
     } 
}.AsQueryable()); 

Vì vậy, khi Find đang kêu gọi GetQuery nội bộ trong lớp GenericRepository nó sẽ nhận được các đơn vị Lịch đúng mà tôi thiết lập trong GetQuery. Nhưng điều này không làm việc tất nhiên. Vì tôi chưa thiết lập phương thức Find của đối tượng giả của mình nên tôi không nhận được bất kỳ thực thể nào.

Vậy phải làm gì? Tất nhiên tôi có lẽ có thể sử dụng nốt ruồi hoặc một số khuôn khổ khác mà mocks tất cả mọi thứ nhưng tôi không muốn làm điều đó. Có điều gì tôi có thể làm trong thiết kế của lớp học hoặc bài kiểm tra để giải quyết vấn đề này không?

Nó không phải là kết thúc của thế giới nếu tôi phải đi với giải pháp hiện tại của tôi nhưng nếu năm tài sản biến thành một int không nullable? Sau đó, tất nhiên tôi sẽ phải thay đổi thực hiện của tôi trong lớp logic nhưng tôi cũng sẽ phải thay đổi thử nghiệm. Tôi muốn tránh điều này.

+1

Tôi nghĩ một cách để thực hiện việc này là 'GetDistinctYearsFromCalendar' phụ thuộc vào 'IRepository'. Bằng cách đó bạn có thể giả lập 'GenericRepository' một bài kiểm tra 'GetDistinctYearsFromCalendar' bị cô lập. Xem http://en.wikipedia.org/wiki/Dependency_injection để biết thêm chi tiết. – Michael

+0

Được rồi, tôi không thấy ctor nội bộ. Bạn có thử nghiệm và mã sản xuất trong cùng một dự án không? Tôi nghĩ rằng đúng cách để làm điều này là làm tiêm phụ thuộc tất cả các cách thức thông qua và bỏ qua các ctor mặc định. – Michael

+0

Tôi nhận ra rằng tôi chỉ có thể viết logic của mình là: return _repository.GetQuery (). Where (c => c.Year.HasValue) .Select (c => c.Year.Value) .Distinct(). OrderBy (c => c) .Reverse(). ToArray(); Không, tôi giữ các bài kiểm tra trong một dự án khác nhưng phơi bày nội bộ cho dự án thử nghiệm với các chỉ thị lắp ráp. Nhưng nếu tôi không bao giờ sử dụng loại nào khác so với GenericRepository thì sao? Có cần thiết phải tiêm phụ thuộc vào tất cả các cách sau đó không? Điều đó có vẻ là giải pháp tốt nhất phải không? Tôi đã bị mù và muốn sử dụng tất cả các chức năng của kho lưu trữ. – John

Trả lời

12

tôi có thể thấy hai cách:

public class MockRepository : IRepository 
{ 
    private List<object> entities; 
    public MockRepository(params object[] entitites) 
    { 
     this.entities = entities.ToList(); 
    } 

    public IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class 
    { 
     return this.entities.OfType<TEntity>().AsQueryable(); 
    } 

    public IEnumerable<TEntity> Find<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class 
    { 
     return GetQuery<TEntity>().Where(predicate).AsEnumerable(); 
    } 
} 

Đó là dễ nhất và cách ưa thích của tôi. Moq không phải là cái búa cho mọi thứ;)

Ngoài ra, nếu bạn thực sự khăng khăng sử dụng Moq (tôi rất thất vọng, nhưng rất cần thiết trong trường hợp này, vì bạn có thể thực hiện kiểm tra trạng thái trên các thực thể được trả về) , bạn có thể làm:

public class GenericRepository : IRepository 
{ 
    public virtual IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class 
    { 
     var entityName = GetEntityName<TEntity>(); 
     return Context.CreateQuery<TEntity>(entityName); 
    } 
    private string GetEntityName<TEntity>() where TEntity : class 
    { 
     return typeof(TEntity).Name; 
    } 
    public IEnumerable<TEntity> Find<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class 
    { 
     return GetQuery<TEntity>().Where(predicate).AsEnumerable(); 
    } 
} 

Và sau đó sử dụng Moq để ghi đè lên hành vi của GetQuery:

var repository = new Mock<GenericRepository> { CallBase = true }; 

repository.Setup(x => x.GetQuery<Foo>()).Returns(theFoos.AsQueryable()); 

Điều gì sẽ xảy ra là Find phương pháp sẽ được thực hiện trên lớp GenericRepository, mà sẽ lần lượt các GetQuery, đã được ghi đè bởi Moq để provi từ tập hợp các thực thể cố định.

Tôi đặt CallBase = true một cách rõ ràng chỉ trong trường hợp bạn cũng có thể làm Tìm ảo, để đảm bảo nó luôn được gọi. Không cần thiết về mặt kỹ thuật nếu Tìm kiếm không phải là ảo, vì nó sẽ luôn luôn được gọi trên lớp thực tế mà giả sử được thừa hưởng/chế nhạo từ đó.

Tôi muốn chọn tùy chọn đầu tiên, đơn giản hơn để hiểu những gì đang diễn ra và có thể được sử dụng lại bên ngoài ngữ cảnh của một thử nghiệm cụ thể (chỉ cần chuyển bất kỳ thực thể nào bạn cần và nó sẽ hoạt động cho mọi thứ).

+0

Vì tôi hiểu rằng việc chế nhạo một phương thức trả về IQueryable không thể kiểm tra được bởi vì bạn phải chạy thử nghiệm tích hợp với cơ sở dữ liệu, tôi tự hỏi liệu nó có thể giả lập phương thức Tìm và trả nó không. Tôi đã thử với It.IsAny <> nhưng nó không hoạt động. – John

0

Gần đây, một công cụ mới có tên là Effort đã xuất hiện cho EF 6+ mà tôi thấy hữu ích cho việc kiểm thử đơn vị đối với một DB giả. Xem http://effort.codeplex.com/wikipage?title=Tutorials&referringTitle=Home.

Thêm nó bằng cách sử dụng gói này quản lý giao diện điều khiển lệnh:

PM> Install-Package Effort.EF6 

Sau đó thêm một giao diện cho DbContext của bạn, chẳng hạn, nếu bạn đang sử dụng cơ sở dữ liệu AdventureWorks (xem https://sql2012kitdb.codeplex.com/):

Sau đó cập nhật của bạn DbContext để thêm hai hàm tạo mới được tham số hóa:

/// 
    /// Create a new context based on database name or connection string. 
    /// 
    /// Database name or connection string 
    public AdventureWorksEntities(string nameOrConnectionString) 
     : base(nameOrConnectionString) 
    { 
     this.Configuration.LazyLoadingEnabled = false; 
    } 

    public AdventureWorksEntities(DbConnection connection) 
     : base(connection, true) 
    { 
     this.Configuration.LazyLoadingEnabled = false; 
    } 

Thêm một hàm tạo giao diện vào kho lưu trữ của bạn:

private IAdventureWorksDbContext _dbContext; 

    public ProductRepository(IAdventureWorksDbContext dbContext) 
    { 
     dbContext.Configuration.AutoDetectChangesEnabled = false; 
     this._dbContext = dbContext; 
    } 

Sau đó thêm một giao diện cho dự án kiểm tra đơn vị của bạn và lớp liên kết:

public interface ITestDatabase : IDisposable 
{ 
    IAdventureWorksDbContext CreateContext(); 

    void Dispose(IAdventureWorksDbContext context); 
} 

Thêm một số dữ liệu giả để dự án kiểm tra đơn vị của bạn:

public class ProductsTestData 
{ 
    public static void AddTestData(IAdventureWorksDbContext dbContext) 
    { 
     dbContext.Products.Add(new Product() { Id = new Guid("23ab9e4e-138a-4223-bb42-1dd176d8583cB"), Name = "Product A", CreatedDate = DateTime.Now, Description = "Product description..." }); 
     dbContext.Products.Add(new Product() { Id = new Guid("97e1835f-4c1b-4b87-a514-4a17c019df00"), Name = "Product B", CreatedDate = DateTime.Now }); 
     dbContext.SaveChanges(); 
    } 
} 

Bây giờ thiết lập của bạn lớp kiểm tra đơn vị :

[TestClass] 
public class ProductsTest 
{ 
    private ITestDatabase _testDatabaseStrategy; 
    private ProductRepository _productRepository; 
    private IAdventureWorksDbContext _context; 

    [TestInitialize] 
    public void SetupTest() 
    { 
     // create the test strategy. This will initialise a new database 
     _testDatabaseStrategy = CreateTestStrategy(); 

     // add test data to the database instance 
     using (_context = _testDatabaseStrategy.CreateContext()) 
     { 
      ProductsTestData.AddTestData(_context); 
      _context.SaveChanges(); 
     } 

     // initialise the repository we are testing 
     _context = _testDatabaseStrategy.CreateContext(); 
     _productRepository = new ProductRepository(_context); 
    } 

    protected ITestDatabase CreateTestStrategy() 
    { 
     return new EffortDatabaseStrategy(); 
    } 

    [TestCleanup] 
    public void CleanupTest() 
    { 
     // dispose of the database and connection 
     _testDatabaseStrategy.Dispose(_context); 
     _context = null; 
    } 

    [TestMethod] 
    public void GetProductsByTagName() 
    { 
     IEnumerable<Product> products = _productRepository.GetProductsByTagName("Tag 1", false); 
     Assert.AreEqual(1, products.Count()); 
    } 

Wh ere EffortDatabaseStrategy là:

public class EffortDatabaseStrategy : ITestDatabase 
{ 
    public EffortDatabaseStrategy() 
    { 
    } 

    private DbConnection _connection; 

    public IAdventureWorksDbContext CreateContext() 
    { 
     if (_connection == null) 
     { 
      _connection = Effort.DbConnectionFactory.CreateTransient(); 
     } 
     var context = new AdventureWorksDbContext(_connection); 

     return context; 
    } 

    public void Dispose(IAdventureWorksDbContext context) 
    { 
     if (context != null) 
     { 
      context.Dispose(); 
     } 
    } 

    public void Dispose() 
    { 
    } 
} 

Để biết đầy đủ chi tiết, vui lòng xem http://www.codeproject.com/Articles/460175/Two-strategies-for-testing-Entity-Framework-Effort?msg=5122027#xx5122027xx.

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