2014-09-30 17 views
5

Tôi đang viết bài kiểm thử đơn vị cho ứng dụng web MVC 5. Tôi đã chế nhạo thử nghiệm HttpContext.Current. Khi chạy sau mã mẫu thử nghiệm httpSessionStateAfter némHttpContext.Current is null after await (only in unit tests)

System.AggregateException: Một hoặc nhiều lỗi xảy ra.
----> System.NullReferenceException: Tham chiếu đối tượng không được đặt thành phiên bản của đối tượng.

Điều này chỉ xảy ra khi tôi chạy thử nghiệm đơn vị. Khi ứng dụng chạy công việc này tốt. Tôi đang sử dụng Nunit 2.6.3 với trình chạy thử nghiệm nhân bản.

var httpSessionStateBefour = System.Web.HttpContext.Current.Session; 
var Person= await Db.Persons.FirstOrDefaultAsync(); 
var httpSessionStateAfter = System.Web.HttpContext.Current.Session; 

Làm cách nào để khắc phục sự cố này?

Đây là cách tôi nhạo báng HttpContext

HttpContext.Current = Fakes.FakeHttpContext();    
HttpContext.Current.Session.Add("IsUserSiteAdmin", true); 
HttpContext.Current.Session.Add("CurrentSite", null); 

public static class Fakes 
{ 
    public static HttpContext FakeHttpContext() 
    { 
     var httpRequest = new HttpRequest("", "http://stackoverflow/", ""); 
     var stringWriter = new StringWriter(); 
     var httpResponce = new HttpResponse(stringWriter); 
     var httpContext = new HttpContext(httpRequest, httpResponce); 

     var sessionContainer = new HttpSessionStateContainer("id", new SessionStateItemCollection(), 
      new HttpStaticObjectsCollection(), 10, true, 
      HttpCookieMode.AutoDetect, 
      SessionStateMode.InProc, false); 

     httpContext.Items["AspSession"] = typeof (HttpSessionState).GetConstructor(
      BindingFlags.NonPublic | BindingFlags.Instance, 
      null, CallingConventions.Standard, 
      new[] {typeof (HttpSessionStateContainer)}, 
      null) 
      .Invoke(new object[] {sessionContainer}); 

     return httpContext; 
    } 
} 
+1

Câu hỏi của bạn chính xác là gì? –

+0

@Tragedian Tôi đoán "tại sao' httpSessionStateAfter' không có nhưng 'httpSessionStateBefour' không phải là?" – DavidG

+2

Bạn đang sử dụng khung kiểm thử đơn vị nào? Bạn có chắc nó hỗ trợ các ngữ cảnh thực thi không? Các phiên bản cũ hơn của NUnit chẳng hạn biết gì về 'await' để chúng không thể cấu hình việc tiếp tục. Trong các ứng dụng ASP.NET tiếp tục được thực hiện trên một thread * ThreadPool * khác nhau được cấu hình với bối cảnh thực hiện ban đầu –

Trả lời

8

HttpContext.Current được coi là một tài sản để làm việc với khá khủng khiếp; nó không hoạt động bên ngoài nhà ASP.NET của nó. Cách tốt nhất để sửa mã của bạn là ngừng tìm kiếm thuộc tính này và tìm cách tách biệt mã đó khỏi mã bạn đang thử nghiệm. Ví dụ: bạn có thể tạo giao diện đại diện cho dữ liệu phiên hiện tại của bạn và hiển thị giao diện đó cho thành phần bạn đang thử nghiệm, với triển khai yêu cầu ngữ cảnh HTTP.


Vấn đề gốc là làm cách nào với cách hoạt động của HttpContext.Current. Thuộc tính này là "huyền diệu" trong khuôn khổ ASP.NET, trong đó nó là duy nhất cho một hoạt động yêu cầu-đáp ứng, nhưng nhảy giữa các chủ đề khi thực hiện yêu cầu nó - nó được chia sẻ có chọn lọc giữa các luồng.

Khi bạn sử dụng HttpContext.Current ngoài đường dẫn xử lý ASP.NET, ma thuật sẽ biến mất. Khi bạn chuyển đổi chủ đề giống như bạn đang ở đây với kiểu lập trình không đồng bộ, thuộc tính là null sau khi tiếp tục.

Nếu bạn hoàn toàn không thể thay đổi mã của mình để loại bỏ sự phụ thuộc khó khăn trên HttpContext.Current, bạn có thể lừa bài kiểm tra này bằng cách tận dụng bối cảnh cục bộ của mình: tất cả các biến trong phạm vi cục bộ khi bạn tuyên bố tiếp tục được cung cấp cho ngữ cảnh tiếp tục .

// Bring the current value into local scope. 
var context = System.Web.HttpContext.Current; 

var httpSessionStateBefore = context.Session; 
var person = await Db.Persons.FirstOrDefaultAsync(); 
var httpSessionStateAfter = context.Session; 

Để rõ ràng, điều này sẽ chỉ hoạt động cho kịch bản hiện tại của bạn. Nếu bạn giới thiệu một await trước điều này trong phạm vi khác, mã sẽ đột nhiên phá vỡ một lần nữa; đây là câu trả lời nhanh chóng và dơ bẩn mà tôi khuyến khích bạn bỏ qua và theo đuổi một giải pháp mạnh mẽ hơn.

+0

Điều đầu tiên tôi đang làm loại thử nghiệm tích hợp. Tôi viết kiểm tra cho controller.It sẽ đi tất cả các con đường xuống cơ sở dữ liệu. Tôi đã sử dụng HttpContext.Current để lấy chi tiết từ Session bên trong DbContext. Trước đó, tôi làm điều đó bên trong phương thức SaveChanges() thực hiện sau một số cuộc gọi không đồng bộ. Thay vào đó bây giờ tôi thêm nó như là một constructor khởi tạo bất động sản mà thực hiện trước khi các cuộc gọi async. Cảm ơn bạn đã giúp đỡ –

9

Trước tiên, tôi khuyên bạn nên cách ly mã của mình càng nhiều càng tốt từ HttpContext.Current; không chỉ điều này sẽ làm cho mã của bạn dễ kiểm tra hơn, nhưng nó sẽ giúp bạn chuẩn bị cho ASP.NET vNext, giống OWIN hơn (không có HttpContext.Current).

Tuy nhiên, điều đó có thể yêu cầu nhiều thay đổi mà bạn có thể chưa sẵn sàng. Để mô phỏng đúng cách HttpContext.Current, bạn cần hiểu cách hoạt động của nó.

HttpContext.Current là biến số trên luồng được điều khiển bởi ASP.NET SynchronizationContext.SynchronizationContext là "ngữ cảnh yêu cầu", thể hiện yêu cầu hiện tại; nó được tạo bởi ASP.NET khi có yêu cầu mới. Tôi có một số MSDN article on SynchronizationContext nếu bạn quan tâm đến nhiều chi tiết hơn.

Như tôi đã giải thích trong tôi async intro blog post, khi bạn await một Task, theo mặc định nó sẽ nắm bắt được "bối cảnh" hiện tại và sử dụng để tiếp tục phương pháp async. Khi phương thức async đang chạy trong ngữ cảnh yêu cầu ASP.NET, "ngữ cảnh" được chụp bởi await là ASP.NET SynchronizationContext. Khi hồ sơ async tiếp tục (có thể trên một chủ đề khác), ASP.NET SynchronizationContext sẽ đặt HttpContext.Current trước khi tiếp tục phương thức async. Đây là cách async/await hoạt động trong máy chủ ASP.NET.

Bây giờ, khi bạn chạy cùng một mã trong thử nghiệm đơn vị, hành vi sẽ khác. Cụ thể, không có ASP.NET SynchronizationContext để đặt HttpContext.Current. Tôi giả định rằng phương pháp thử nghiệm đơn vị của bạn trả về Task, trong trường hợp này NUnit không cung cấp SynchronizationContext. Vì vậy, khi hồ sơ async tiếp tục (có thể trên một chủ đề khác), HttpContext.Current của nó có thể không giống nhau.

Có một vài cách khác nhau để khắc phục sự cố này. Một tùy chọn là viết SynchronizationContext của riêng bạn, giữ nguyên HttpContext.Current, giống như ASP.NET. Một lựa chọn dễ dàng hơn (nhưng kém hiệu quả hơn) là sử dụng a SynchronizationContext that I wrote called AsyncContext, đảm bảo phương pháp async sẽ tiếp tục trên cùng một chuỗi. Bạn sẽ có thể cài đặt my AsyncEx library from NuGet và sau đó bọc các phương pháp thử nghiệm đơn vị của bạn bên trong một cuộc gọi đến AsyncContext.Run. Lưu ý rằng các phương pháp kiểm tra đơn vị hiện nay đồng bộ:

[Test] 
public void MyTest() 
{ 
    AsyncContext.Run(async() => 
    { 
    // Test logic goes here: set HttpContext.Current, etc. 
    }); 
} 
+0

Tác vụ 'AsyncContext' của bạn có hoạt động với' await' lồng nhau không? Tôi vừa thử nó trong dự án .NET 4.5, nơi phương thức thử nghiệm của tôi đặt 'HttpContext.Current' thành một giả mạo và sau đó gọi một phương thức sử dụng' await' bên trong.Đối với cấp cao nhất 'await' nó hoạt động tốt (và nó thậm chí không có 'AsyncContext', nhưng ngay sau khi một' chờ đợi '' 'HttpContext.Current' là null một lần nữa' chờ đợi 'đang gọi phương thức dẫn xuất' FindByNameAsync () 'từ một lớp bắt nguồn từ' Microsoft.AspNet.Identity.UserManager'. – JustAMartin

+0

@JustAMartin: 'AsyncContext' chỉ đảm bảo rằng phương thức sẽ tiếp tục trên cùng một luồng * *. Nó không làm bất cứ điều gì đặc biệt với' HttpContext.Current' Nó có vẻ như một số mã ASP.NET có thể được xóa ra 'HttpContext.Current'. Trừ khi mã của bạn sử dụng' ConfigureAwait (false) ', mà sẽ gây ra một thread nhảy. –

1

tôi đến câu hỏi này khi có một vấn đề trong mã của tôi ... nơi HttpContext.Current là null sau một chờ đợi trong một Async MVC Hành động. Tôi đăng bài này ở đây bởi vì những người khác như tôi có thể đáp xuống đây.

Khuyến nghị chung của tôi là lấy bất kỳ thứ gì bạn muốn từ phiên vào biến cục bộ, giống như những người khác ở trên thảo luận, nhưng không lo lắng về việc giữ ngữ cảnh và thay vào đó chỉ lo lắng về việc lấy các mục thực tế bạn muốn.

public async Task<ActionResult> SomeAction(SomeModel model) 
{ 
int id = (int)HttpContext.Current.Session["Id"]; 

/* Session Exists Here */ 

var somethingElseAsyncModel = await GetSomethingElseAsync(model); 

/* Session is Null Here */ 

// Do something with id, thanks to the fact we got it when we could 
} 
+0

Tôi đánh giá cao nó và upvoted, nhưng người đàn ông, muốn bạn đã có – Barry

+0

@ Barry, bạn có thể cố gắng chỉ một biến vào nó (phiên), nhưng tôi đã không cố gắng như vậy. Có vẻ an toàn hơn và sạch hơn với tôi để chỉ lấy những gì tôi cần, nhưng đó là giả định bạn sẽ không được modi làm chết phiên trong hầu hết các trường hợp. Ngoài ra, tôi thường cố gắng sử dụng phiên càng ít càng tốt. Sử dụng quá mức phiên (suy nghĩ toàn cầu) có thể dẫn đến hành vi không xác định khi người dùng bắt đầu sử dụng hai cửa sổ/tab trình duyệt riêng biệt. – Greg

+0

Phải đồng ý, tôi đã kết thúc theo dõi mô hình của bạn để lấy dữ liệu cần thiết trước. Tôi đang làm việc để chuyển đổi mã kế thừa dựa nhiều vào phiên, nó chắc chắn không an toàn ở nhiều cấp độ. Tôi đánh giá cao thông tin về điều này, đã giúp tôi đáng kể! – Barry

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