2013-10-27 3 views
26

나는 (자습서 Create an ASP.NET MVC 5 App with Facebook and Google OAuth2 and OpenID Sign-on (C#) 참조) 새 속성을 추가하여 ApplicationUser 클래스를 확장하고있어단위 테스트 ASP.NET MVC5 앱

public class ApplicationUser : IdentityUser 
{ 
    public DateTime BirthDate { get; set; } 
} 

지금 나는 것을 확인하는 단위 테스트를 만들려면 내 AccountController가 BirthDate를 올바르게 저장하고 있습니다.

나는 controller.Register 방법은 참조 용으로 여기를 포함하고있어 MVC5에 의해 생성 된 상용구 코드가 되나 TestUserStore

[TestMethod] 
public void Register() 
{ 
    // Arrange 
    var userManager = new UserManager<ApplicationUser>(new TestUserStore<ApplicationUser>()); 
    var controller = new AccountController(userManager); 

    // This will setup a fake HttpContext using Moq 
    controller.SetFakeControllerContext(); 

    // Act 
    var result = 
     controller.Register(new RegisterViewModel 
     { 
      BirthDate = TestBirthDate, 
      UserName = TestUser, 
      Password = TestUserPassword, 
      ConfirmPassword = TestUserPassword 
     }).Result; 

    // Assert 
    Assert.IsNotNull(result); 

    var addedUser = userManager.FindByName(TestUser); 
    Assert.IsNotNull(addedUser); 
    Assert.AreEqual(TestBirthDate, addedUser.BirthDate); 
} 

라는 인 메모리 사용자 저장소를 만들었습니다.

// POST: /Account/Register 
[HttpPost] 
[AllowAnonymous] 
[ValidateAntiForgeryToken] 
public async Task<ActionResult> Register(RegisterViewModel model) 
{ 
    if (ModelState.IsValid) 
    { 
     var user = new ApplicationUser() { UserName = model.UserName, BirthDate = model.BirthDate }; 
     var result = await UserManager.CreateAsync(user, model.Password); 
     if (result.Succeeded) 
     { 
      await SignInAsync(user, isPersistent: false); 
      return RedirectToAction("Index", "Home"); 
     } 
     else 
     { 
      AddErrors(result); 
     } 
    } 

    // If we got this far, something failed, redisplay form 
    return View(model); 
} 

내가 등록 (Register)을 호출하면 문제가 발생하는 SignInAsync가 호출됩니다. 최하층

private async Task SignInAsync(ApplicationUser user, bool isPersistent) 
{ 
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie); 
    var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie); 
    AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity); 
} 

은 상용구 코드는 토막

private IAuthenticationManager AuthenticationManager 
{ 
    get 
    { 
     return HttpContext.GetOwinContext().Authentication; 
    } 
} 

problm의 루트가 발생하는 곳이다를 포함한다. GetOwinContext에 대한이 호출은 내가 모의 할 수없는 확장 메소드이며 스텁으로 대체 할 수 없습니다 (물론 상용구 코드를 변경하지 않는 한). 나는이 테스트를 실행하면

은 내가 ASP.NET MVC 팀이 코드를 테스트 할 수 있도록하기 위해 매우 열심히 노력 예외를 이전 릴리스에서

Test method MVCLabMigration.Tests.Controllers.AccountControllerTest.Register threw exception: 
System.AggregateException: One or more errors occurred. ---> System.NullReferenceException: Object reference not set to an instance of an object. 
at System.Web.HttpContextBaseExtensions.GetOwinEnvironment(HttpContextBase context) 
at System.Web.HttpContextBaseExtensions.GetOwinContext(HttpContextBase context) 
at MVCLabMigration.Controllers.AccountController.get_AuthenticationManager() in AccountController.cs: line 330 
at MVCLabMigration.Controllers.AccountController.<SignInAsync>d__40.MoveNext() in AccountController.cs: line 336 

를 얻을. 이제는 AccountController를 테스트하는 것이 쉽지 않을 것 같습니다. 나는 몇 가지 선택권이있다. 이 확장 메서드를 호출하고 그 수준에서이 문제를

  • 설정 테스트 목적으로 OWin 파이프 라인을 처리하지 않도록

    나는

    1. 보일러 플레이트 코드를 수정할 수 있습니다

    2. AuthN/AuthZ 인프라가 필요한 테스트 코드 작성을 피하십시오 (적절한 옵션이 아님)

    어떤 도로가 더 좋은지 잘 모르겠습니다. 어느 쪽이든이 문제를 해결할 수 있습니다. 제 질문은 가장 좋은 전략입니다.

    참고 : 예, 필자가 작성하지 않은 코드를 테스트 할 필요가 없음을 알고 있습니다. 제공된 UserManager 인프라 MVC5는 인프라 스트럭처입니다.하지만 ApplicationManager에 대한 수정 사항을 확인하는 테스트를 작성하거나 사용자 역할에 따라 동작을 확인하는 코드를 작성하려면 UserManager를 사용하여 테스트해야합니다.

  • +0

    안녕하세요. 당신이 언급 한 튜토리얼을 가르쳐주세요. 또한 Register 메소드 구현으로 질문을 업데이트 할 수 있다면 도움이 될 것입니다. – Spock

    +0

    유닛 테스트에서 참조 할 때 HttpContext는 null이됩니다. 사용하는 속성 값을 반환하려면 모의 HttpContext 객체를 삽입해야 할 수도 있습니다. –

    +0

    모의 설정을했는데 위의 샘플 코드가 아니었지만 지금 추가했습니다. –

    답변

    26

    나는 내 자신의 질문에 답하고 있으므로 좋은 대답이라고 생각하면 커뮤니티에서 감각을 얻을 수 있습니다.

    1 단계 : 생성 된 AccountController를 수정하여 Backing 필드를 사용하여 AuthenticationManager에 대한 속성 설정자를 제공합니다.2 단계

    // Add this private variable 
    private IAuthenticationManager _authnManager; 
    
    // Modified this from private to public and add the setter 
    public IAuthenticationManager AuthenticationManager 
    { 
        get 
        { 
         if (_authnManager == null) 
          _authnManager = HttpContext.GetOwinContext().Authentication; 
         return _authnManager; 
        } 
        set { _authnManager = value; } 
    } 
    

    :

    [TestMethod] 
    public void Register() 
    { 
        // Arrange 
        var userManager = new UserManager<ApplicationUser>(new TestUserStore<ApplicationUser>()); 
        var controller = new AccountController(userManager); 
        controller.SetFakeControllerContext(); 
    
        // Modify the test to setup a mock IAuthenticationManager 
        var mockAuthenticationManager = new Mock<IAuthenticationManager>(); 
        mockAuthenticationManager.Setup(am => am.SignOut()); 
        mockAuthenticationManager.Setup(am => am.SignIn()); 
    
        // Add it to the controller - this is why you have to make a public setter 
        controller.AuthenticationManager = mockAuthenticationManager.Object; 
    
        // Act 
        var result = 
         controller.Register(new RegisterViewModel 
         { 
          BirthDate = TestBirthDate, 
          UserName = TestUser, 
          Password = TestUserPassword, 
          ConfirmPassword = TestUserPassword 
         }).Result; 
    
        // Assert 
        Assert.IsNotNull(result); 
    
        var addedUser = userManager.FindByName(TestUser); 
        Assert.IsNotNull(addedUser); 
        Assert.AreEqual(TestBirthDate, addedUser.BirthDate); 
    } 
    

    지금 테스트를 통과 Microsoft.OWin.IAuthenticationManager 인터페이스에 대한 모의를 추가하는 단위 테스트를 수정합니다.

    좋은 아이디어? 나쁜 생각?

    +4

    아마도'IAuthenticationManager'을 private readonly 필드로 만들고 생성자를 통해 그것을 설정할 것입니다. –

    +0

    정말 작은 변화 일 뿐이지 만 AuthenticationManager의 get 속성은 ??을 사용하여 작성 될 수 있으므로 null 검사가 불필요합니다. – viniciushana

    +1

    모의 (Mock) 란 무엇입니까? 어떻게 사용할 수 있습니까? –

    3

    나는 당신과 비슷한 해결책을 사용했다. IAuthenticationManager를 조롱했는데, 로그인 코드는 생성자 주입을 통해 IAuthenticationManager를 사용하는 LoginManager 클래스에있다. 내 의존성 등록 Unity을 사용하고

    public LoginHandler(HttpContextBase httpContext, IAuthenticationManager authManager) 
        { 
         _httpContext = httpContext; 
         _authManager = authManager; 
        } 
    

    : (그러나

    public static void RegisterTypes(IUnityContainer container) 
        { 
         container.RegisterType<HttpContextBase>(
          new InjectionFactory(_ => new HttpContextWrapper(HttpContext.Current))); 
         container.RegisterType<IOwinContext>(new InjectionFactory(c => c.Resolve<HttpContextBase>().GetOwinContext())); 
         container.RegisterType<IAuthenticationManager>(
          new InjectionFactory(c => c.Resolve<IOwinContext>().Authentication)); 
         container.RegisterType<ILoginHandler, LoginHandler>(); 
         // Further registrations here... 
        } 
    

    , 내 유니티 등록을 테스트하고 싶습니다,이 (a)는 HttpContext.Current 날조없이 까다로운 입증했다 하드) 및 (b) GetOwinContext() - 당신이 발견 한 것처럼 직접 수행하는 것은 불가능합니다.

    저는 Phil Haack의 HttpSimulator 형태의 솔루션과 HttpContext의 일부 조작을 통해 기본 Owin environment을 생성했습니다. 지금까지 나는 하나의 더미 Owin 변수를 설정하면 GetOwinContext()가 작동하지만 YMMV를 만들기에 충분하다는 것을 발견했습니다.

    public static class HttpSimulatorExtensions 
    { 
        public static void SimulateRequestAndOwinContext(this HttpSimulator simulator) 
        { 
         simulator.SimulateRequest(); 
         Dictionary<string, object> owinEnvironment = new Dictionary<string, object>() 
          { 
           {"owin.RequestBody", null} 
          }; 
         HttpContext.Current.Items.Add("owin.Environment", owinEnvironment); 
        }   
    } 
    
    [TestClass] 
    public class UnityConfigTests 
    { 
        [TestMethod] 
        public void RegisterTypes_RegistersAllDependenciesOfHomeController() 
        { 
         IUnityContainer container = UnityConfig.GetConfiguredContainer(); 
         HomeController controller; 
    
         using (HttpSimulator simulator = new HttpSimulator()) 
         { 
          simulator.SimulateRequestAndOwinContext(); 
          controller = container.Resolve<HomeController>(); 
         } 
    
         Assert.IsNotNull(controller); 
        } 
    } 
    

    HttpSimulator 당신의 SetFakeControllerContext() 메소드는 일을하지만 통합 테스트를위한 유용한 도구처럼 보이는 경우 과잉 될 수있다.

    +0

    HttpSimulator()에 using 문을 사용하는 이유는 무엇입니까? 테스트 프로젝트의 다른 장소에서이 기능을 사용하기를 원한다고 가정 해 봅시다. (그리고 화려한 대답에 대한 감사, 내 문제를 멋지게 해결). – Hanshan

    +1

    @nulliusinverba HttpSimulator를 삭제하면 HttpContext.Current가 null로 다시 설정되므로 다른 테스트가 손상 될 위험이 없습니다. 이것을 다른 곳에서 사용 가능하게하는 것에 대한 좋은 질문 : 저는 SimulateRequestAndOwinContext를 private에서 확장 메소드로 바꾸었습니다.이 메소드는 작업을 수행하고 구문을 원래의 'simulator.SimulateRequest()'에 가깝게 유지합니다. (HttpSimulator를 서브 클래스 화하고 SimulateRequest()의 가상 오버로드를 오버라이드 할 수도 있습니다.) – Blisco

    4

    필자의 요구는 비슷하지만 AccountController의 순수한 단위 테스트를 원하지 않는다는 것을 깨달았습니다. 오히려 자연 서식지에 최대한 가깝게있는 환경에서 테스트하고 싶습니다 (원하는 경우 통합 테스트). 따라서 주변 물건을 조롱하고 싶지는 않지만 실제 물건을 사용하십시오. 내가 쓸 수있는 코드는 거의 없습니다.

    HttpContextBaseExtensions.GetOwinContext 메서드도 내 방식으로 전환되었으므로 Blisco의 힌트에 매우 만족했습니다. 지금 내 솔루션의 가장 중요한 부분은 다음과 같습니다

    /// <summary> Set up an account controller with just enough context to work through the tests. </summary> 
    /// <param name="userManager"> The user manager to be used </param> 
    /// <returns>A new account controller</returns> 
    private static AccountController SetupAccountController(ApplicationUserManager userManager) 
    { 
        AccountController controller = new AccountController(userManager); 
        Uri url = new Uri("https://localhost/Account/ForgotPassword"); // the real string appears to be irrelevant 
        RouteData routeData = new RouteData(); 
    
        HttpRequest httpRequest = new HttpRequest("", url.AbsoluteUri, ""); 
        HttpResponse httpResponse = new HttpResponse(null); 
        HttpContext httpContext = new HttpContext(httpRequest, httpResponse); 
        Dictionary<string, object> owinEnvironment = new Dictionary<string, object>() 
        { 
         {"owin.RequestBody", null} 
        }; 
        httpContext.Items.Add("owin.Environment", owinEnvironment); 
        HttpContextWrapper contextWrapper = new HttpContextWrapper(httpContext); 
    
        ControllerContext controllerContext = new ControllerContext(contextWrapper, routeData, controller); 
        controller.ControllerContext = controllerContext; 
        controller.Url = new UrlHelper(new RequestContext(contextWrapper, routeData)); 
        // We have not found out how to set up this UrlHelper so that we get a real callbackUrl in AccountController.ForgotPassword. 
    
        return controller; 
    } 
    

    나는 아직 (특히, 내가 UrlHelper이 ForgotPassword 방법에 적절한 URL을 생성 할 수 없었다) 모든 작업을 얻기 위해 성공했지만하지 않은 내 요구의 대부분은 지금 다루고 있습니다.

    +0

    더 현실적인 테스트를 위해 +1. 다른 것들은 세분화되는 동안 똑같이 유효하지만 때로는 테스트 레벨에서 개념 증명을 원할 때가 있습니다.이 솔루션은 (필자의 코드 오믈렛보다 좋지만) –