2010-04-29 18 views
9

Tôi đang cố gắng kiểm tra đơn vị/xác minh rằng phương thức đang được gọi dựa trên sự phụ thuộc, bởi hệ thống đang được kiểm tra (SUT).Thử nghiệm đơn vị với Mocks khi SUT đang tận dụng Task Parallel Libaray

  • Mức độ trầm trọng là IFoo.
  • Lớp phụ thuộc là IBar.
  • IBar được triển khai dưới dạng Bar.
  • Thanh sẽ gọi lệnh Start() trên IFoo trong một nhiệm vụ mới (System.Threading.Tasks.), Khi Start() được gọi trên cá thể Bar.

Unit Test (Moq):

[Test] 
    public void StartBar_ShouldCallStartOnAllFoo_WhenFoosExist() 
    { 
     //ARRANGE 

     //Create a foo, and setup expectation 
     var mockFoo0 = new Mock<IFoo>(); 
     mockFoo0.Setup(foo => foo.Start()); 

     var mockFoo1 = new Mock<IFoo>(); 
     mockFoo1.Setup(foo => foo.Start()); 


     //Add mockobjects to a collection 
     var foos = new List<IFoo> 
         { 
          mockFoo0.Object, 
          mockFoo1.Object 
         }; 

     IBar sutBar = new Bar(foos); 

     //ACT 
     sutBar.Start(); //Should call mockFoo.Start() 

     //ASSERT 
     mockFoo0.VerifyAll(); 
     mockFoo1.VerifyAll(); 
    } 

Thực hiện Ibar như Bar:

class Bar : IBar 
    { 
     private IEnumerable<IFoo> Foos { get; set; } 

     public Bar(IEnumerable<IFoo> foos) 
     { 
      Foos = foos; 
     } 

     public void Start() 
     { 
      foreach(var foo in Foos) 
      { 
       Task.Factory.StartNew(
        () => 
         { 
          foo.Start(); 
         }); 
      } 
     } 
    } 

Moq Ngoại lệ:

*Moq.MockVerificationException : The following setups were not matched: 
IFoo foo => foo.Start() (StartBar_ShouldCallStartOnAllFoo_WhenFoosExist() in 
FooBarTests.cs: line 19)* 
+2

Có lý do cụ thể nào không để viết một bản thực thi giả lập đơn giản về chính bản thân bạn và sử dụng thay thế đó không? –

Trả lời

7

@dpurrington & @StevenH: Nếu chúng ta bắt đầu đưa loại công cụ này trong mã của chúng tôi

sut.Start(); 
Thread.Sleep(TimeSpan.FromSeconds(1)); 

và chúng tôi có hàng ngàn "đơn vị" kiểm tra sau đó kiểm tra của chúng tôi bắt đầu chạy vào phút thay vì giây. Nếu bạn đã có ví dụ 1000 bài kiểm tra đơn vị, sẽ rất khó để các bài kiểm tra của bạn chạy dưới 5 giây nếu ai đó đã đi và vứt bỏ cơ sở mã thử nghiệm bằng Thread.Sleep.

Tôi khuyên rằng đây là thực tiễn không tốt, trừ khi chúng tôi đang thực hiện thử nghiệm Tích hợp một cách rõ ràng.

Đề xuất của tôi sẽ là sử dụng giao diện System.Concurrency.IScheduler từ System.CoreEx.dll và tiêm triển khai TaskPoolScheduler.

Đây là đề nghị của tôi cho cách này nên được thực hiện

using System.Collections.Generic; 
using System.Concurrency; 
using Moq; 
using NUnit.Framework; 

namespace StackOverflowScratchPad 
{ 
    public interface IBar 
    { 
     void Start(IEnumerable<IFoo> foos); 
    } 

    public interface IFoo 
    { 
     void Start(); 
    } 

    public class Bar : IBar 
    { 
     private readonly IScheduler _scheduler; 

     public Bar(IScheduler scheduler) 
     { 
      _scheduler = scheduler; 
     } 

     public void Start(IEnumerable<IFoo> foos) 
     { 
      foreach (var foo in foos) 
      { 
       var foo1 = foo; //Save to local copy, as to not access modified closure. 
       _scheduler.Schedule(foo1.Start); 
      } 
     } 
    } 

    [TestFixture] 
    public class MyTestClass 
    { 
     [Test] 
     public void StartBar_ShouldCallStartOnAllFoo_WhenFoosExist() 
     { 
      //ARRANGE 
      TestScheduler scheduler = new TestScheduler(); 
      IBar sutBar = new Bar(scheduler); 

      //Create a foo, and setup expectation 
      var mockFoo0 = new Mock<IFoo>(); 
      mockFoo0.Setup(foo => foo.Start()); 

      var mockFoo1 = new Mock<IFoo>(); 
      mockFoo1.Setup(foo => foo.Start()); 

      //Add mockobjects to a collection 
      var foos = new List<IFoo> 
         { 
          mockFoo0.Object, 
          mockFoo1.Object 
         }; 

      //ACT 
      sutBar.Start(foos); //Should call mockFoo.Start() 
      scheduler.Run(); 

      //ASSERT 
      mockFoo0.VerifyAll(); 
      mockFoo1.VerifyAll(); 
     } 
    } 
} 

này bây giờ cho phép kiểm tra để chạy ở tốc độ cao mà không cần bất kỳ Thread.Sleep.

Lưu ý rằng các hợp đồng đã được sửa đổi để chấp nhận IScheduler trong hàm tạo Bar (cho Dependency Injection) và IEnumerable hiện được chuyển cho phương thức IBar.Start. Tôi hy vọng điều này có ý nghĩa tại sao tôi thực hiện những thay đổi này.

Tốc độ thử nghiệm là lợi ích đầu tiên và rõ ràng nhất khi thực hiện việc này. Lợi ích thứ hai và có thể quan trọng hơn khi thực hiện điều này là khi bạn giới thiệu sự tương tranh phức tạp hơn với mã của bạn, điều này làm cho việc kiểm thử trở nên khó khăn. Giao diện IScheduler và TestScheduler có thể cho phép bạn chạy các phép kiểm thử đơn vị xác định ngay cả khi đối mặt với sự tương tranh phức tạp hơn.

+0

Tôi đồng ý với điểm ban đầu của bạn về giải pháp của tôi. Tôi nghĩ rằng tôi đã chọn sử dụng các sự kiện thay vì một lịch trình, nhưng dù bằng cách nào, tốt hơn câu trả lời của tôi. – dpurrington

+0

Thật không may điều này không còn hoạt động, như tất cả các phương pháp trên [System.Threading.Tasks.TaskScheduler] (http://msdn.microsoft.com/en-us/library/system.threading.tasks.taskscheduler.aspx) được niêm phong . –

+2

@Richard: Lưu ý rằng loại được tiêm là giao diện IScheduler mà TestScheduler thực hiện. Tôi không cố gắng ghi đè lên các phương thức trên TaskSheduler, tôi tận dụng thực tế là cả TaskScheduler và TestScheduler đều triển khai giao diện IScheduler và như vậy có thể hoán đổi cho nhau. –

0

xét nghiệm của bạn sử dụng quá nhiều chi tiết thực hiện, IEnumerable<IFoo> loại. Bất cứ khi nào tôi phải bắt đầu thử nghiệm với IEnumerable nó luôn luôn tạo ra một số ma sát.

0

Thread.Sleep() chắc chắn là một ý tưởng tồi. Tôi đã đọc trên SO nhiều lần qua rằng "ứng dụng thực sự không ngủ". Làm điều đó như bạn sẽ nhưng tôi đồng ý với tuyên bố đó. Đặc biệt là trong các bài kiểm tra đơn vị. Nếu mã thử nghiệm của bạn tạo ra các lỗi sai, các thử nghiệm của bạn sẽ trở nên giòn.

Gần đây, tôi đã viết một số thử nghiệm để chờ các tác vụ song song hoàn thành việc thực thi và tôi nghĩ tôi sẽ chia sẻ giải pháp của mình. Tôi nhận ra đây là một bài cũ nhưng tôi nghĩ rằng nó sẽ cung cấp giá trị cho những người tìm kiếm một giải pháp.

Việc triển khai của tôi liên quan đến việc sửa đổi lớp đang được kiểm tra và phương pháp đang được kiểm tra.

class Bar : IBar 
{ 
    private IEnumerable<IFoo> Foos { get; set; } 
    internal CountdownEvent FooCountdown; 

    public Bar(IEnumerable<IFoo> foos) 
    { 
     Foos = foos; 
    } 

    public void Start() 
    { 
     FooCountdown = new CountdownEvent(foo.Count); 

     foreach(var foo in Foos) 
     { 
      Task.Factory.StartNew(() => 
      { 
       foo.Start(); 

       // once a worker method completes, we signal the countdown 
       FooCountdown.Signal(); 
      }); 
     } 
    } 
} 

Đếm ngược Các đối tượng tiện ích thuận tiện khi bạn thực hiện nhiều tác vụ song song và chờ đợi hoàn thành (như khi chúng tôi chờ thử nghiệm). Hàm khởi tạo khởi tạo với số lần nó được báo hiệu trước khi nó báo hiệu mã đang đợi xử lý xong.

Lý do công cụ sửa đổi truy cập nội bộ được sử dụng cho CountdownEvent là vì tôi thường đặt thuộc tính và phương thức thành nội bộ khi kiểm tra đơn vị cần truy cập vào chúng. Sau đó tôi thêm một thuộc tính assembly mới trong assembly theo tập tin Properties\AssemblyInfo.cs của test để các internals được tiếp xúc với một dự án thử nghiệm.

[assembly: InternalsVisibleTo("FooNamespace.UnitTests")] 

Trong ví dụ này, FooCountdown sẽ đợi để được báo hiệu 3 lần nếu có 3 đối tượng foo trong Foos.

Bây giờ, đây là cách bạn đợi FooCountdown xử lý tín hiệu hoàn thành để bạn có thể tiếp tục cuộc sống và bỏ qua các chu kỳ CPU trên Thread.Sleep().

[Test] 
public void StartBar_ShouldCallStartOnAllFoo_WhenFoosExist() 
{ 
    //ARRANGE 

    var mockFoo0 = new Mock<IFoo>(); 
    mockFoo0.Setup(foo => foo.Start()); 

    var mockFoo1 = new Mock<IFoo>(); 
    mockFoo1.Setup(foo => foo.Start()); 


    //Add mockobjects to a collection 
    var foos = new List<IFoo> { mockFoo0.Object, mockFoo1.Object }; 

    IBar sutBar = new Bar(foos); 

    //ACT 
    sutBar.Start(); //Should call mockFoo.Start() 
    sutBar.FooCountdown.Wait(); // this blocks until all parallel tasks in sutBar complete 

    //ASSERT 
    mockFoo0.VerifyAll(); 
    mockFoo1.VerifyAll(); 
} 
Các vấn đề liên quan