[c#] ASP.NET Core에서 IPrincipal 모의

단위 테스트를 작성중인 ASP.NET MVC Core 애플리케이션이 있습니다. 작업 방법 중 하나는 일부 기능에 사용자 이름을 사용합니다.

SettingsViewModel svm = _context.MySettings(User.Identity.Name);

단위 테스트에서 분명히 실패합니다. 나는 둘러 보았고 모든 제안은 .NET 4.5에서 HttpContext를 모의합니다. 더 나은 방법이 있다고 확신합니다. IPrincipal을 주입하려고했지만 오류가 발생했습니다. 그리고 나는 이것을 시도했습니다 (절망 때문에).

public IActionResult Index(IPrincipal principal = null) {
    IPrincipal user = principal ?? User;
    SettingsViewModel svm = _context.MySettings(user.Identity.Name);
    return View(svm);
}

그러나 이것은 또한 오류를 던졌습니다. 문서에서도 아무것도 찾을 수 없습니다 …



답변

컨트롤러의는 액세스되는 관통 컨트롤러의 . 후자는 저장 내에 .User HttpContextControllerContext

사용자를 설정하는 가장 쉬운 방법은 구성된 사용자와 다른 HttpContext를 할당하는 것입니다. 우리는 DefaultHttpContext이 목적을 위해 사용할 수 있습니다 . 그렇게하면 모든 것을 조롱 할 필요가 없습니다. 그런 다음 컨트롤러 컨텍스트 내에서 해당 HttpContext를 사용하고 컨트롤러 인스턴스에 전달합니다.

var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
{
    new Claim(ClaimTypes.Name, "example name"),
    new Claim(ClaimTypes.NameIdentifier, "1"),
    new Claim("custom-claim", "example claim value"),
}, "mock"));

var controller = new SomeController(dependencies…);
controller.ControllerContext = new ControllerContext()
{
    HttpContext = new DefaultHttpContext() { User = user }
};

고유 한 ClaimsIdentity을 만들 때 authenticationType생성자에 명시 적을 전달해야합니다 . 이것은 IsAuthenticated올바르게 작동하는지 확인 합니다 (사용자가 인증되었는지 여부를 확인하기 위해 코드에서 사용하는 경우).


답변

이전 버전에서는 User컨트롤러에서 직접 설정할 수 있었기 때문에 매우 쉬운 단위 테스트가 가능했습니다.

당신의 소스 코드를 보면 ControllerBase 당신은이 것을 알 User에서 추출됩니다 HttpContext.

/// <summary>
/// Gets the <see cref="ClaimsPrincipal"/> for user associated with the executing action.
/// </summary>
public ClaimsPrincipal User => HttpContext?.User;

컨트롤러는 HttpContext비아에 액세스합니다.ControllerContext

/// <summary>
/// Gets the <see cref="Http.HttpContext"/> for the executing action.
/// </summary>
public HttpContext HttpContext => ControllerContext.HttpContext;

이 두 가지는 읽기 전용 속성임을 알 수 있습니다. 좋은 소식은 ControllerContext속성 이 그 가치를 설정할 수 있도록 허용한다는 것입니다.

그래서 목표는 그 물체에 도달하는 것입니다. In Core HttpContext는 추상적이므로 모의하기가 훨씬 쉽습니다.

컨트롤러가 다음과 같다고 가정합니다.

public class MyController : Controller {
    IMyContext _context;

    public MyController(IMyContext context) {
        _context = context;
    }

    public IActionResult Index() {
        SettingsViewModel svm = _context.MySettings(User.Identity.Name);
        return View(svm);
    }

    //...other code removed for brevity 
}

Moq를 사용하면 테스트는 다음과 같이 보일 수 있습니다.

public void Given_User_Index_Should_Return_ViewResult_With_Model() {
    //Arrange 
    var username = "FakeUserName";
    var identity = new GenericIdentity(username, "");

    var mockPrincipal = new Mock<ClaimsPrincipal>();
    mockPrincipal.Setup(x => x.Identity).Returns(identity);
    mockPrincipal.Setup(x => x.IsInRole(It.IsAny<string>())).Returns(true);

    var mockHttpContext = new Mock<HttpContext>();
    mockHttpContext.Setup(m => m.User).Returns(mockPrincipal.Object);

    var model = new SettingsViewModel() {
        //...other code removed for brevity
    };

    var mockContext = new Mock<IMyContext>();
    mockContext.Setup(m => m.MySettings(username)).Returns(model);

    var controller = new MyController(mockContext.Object) {
        ControllerContext = new ControllerContext {
            HttpContext = mockHttpContext.Object
        }
    };

    //Act
    var viewResult = controller.Index() as ViewResult;

    //Assert
    Assert.IsNotNull(viewResult);
    Assert.IsNotNull(viewResult.Model);
    Assert.AreEqual(model, viewResult.Model);
}


답변

기존 클래스를 사용하고 필요할 때만 조롱 할 수도 있습니다.

var user = new Mock<ClaimsPrincipal>();
_controller.ControllerContext = new ControllerContext
{
    HttpContext = new DefaultHttpContext
    {
        User = user.Object
    }
};


답변

내 경우, 나는의 사용을 만드는 데 필요한 Request.HttpContext.User.Identity.IsAuthenticated, Request.HttpContext.User.Identity.Name및 일부 비즈니스 로직은 컨트롤러의 외부 앉아. 이에 대해 Nkosi, Calin 및 Poke의 답변을 조합하여 사용할 수있었습니다.

var identity = new Mock<IIdentity>();
identity.SetupGet(i => i.IsAuthenticated).Returns(true);
identity.SetupGet(i => i.Name).Returns("FakeUserName");

var mockPrincipal = new Mock<ClaimsPrincipal>();
mockPrincipal.Setup(x => x.Identity).Returns(identity.Object);

var mockAuthHandler = new Mock<ICustomAuthorizationHandler>();
mockAuthHandler.Setup(x => x.CustomAuth(It.IsAny<ClaimsPrincipal>(), ...)).Returns(true).Verifiable();

var controller = new MyController(...);

var mockHttpContext = new Mock<HttpContext>();
mockHttpContext.Setup(m => m.User).Returns(mockPrincipal.Object);

controller.ControllerContext = new ControllerContext();
controller.ControllerContext.HttpContext = new DefaultHttpContext()
{
    User = mockPrincipal.Object
};

var result = controller.Get() as OkObjectResult;
//Assert results

mockAuthHandler.Verify();


답변

추상 팩토리 패턴을 구현하려고합니다.

사용자 이름을 제공하기 위해 특별히 공장 용 인터페이스를 만듭니다.

그런 다음을 제공하는 구체적인 클래스 User.Identity.Name와 테스트에 적합한 다른 하드 코딩 된 값을 제공 하는 클래스를 제공합니다 .

그런 다음 프로덕션 코드와 테스트 코드에 따라 적절한 구체적인 클래스를 사용할 수 있습니다. 공장을 매개 변수로 전달하거나 일부 구성 값을 기반으로 올바른 공장으로 전환하려고 할 수 있습니다.

interface IUserNameFactory
{
    string BuildUserName();
}

class ProductionFactory : IUserNameFactory
{
    public BuildUserName() { return User.Identity.Name; }
}

class MockFactory : IUserNameFactory
{
    public BuildUserName() { return "James"; }
}

IUserNameFactory factory;

if(inProductionMode)
{
    factory = new ProductionFactory();
}
else
{
    factory = new MockFactory();
}

SettingsViewModel svm = _context.MySettings(factory.BuildUserName());


답변

컨트롤러를 직접 치고 AutoFac과 같은 DI를 사용하고 싶습니다. 이렇게하려면 먼저 등록 ContextController합니다.

var identity = new GenericIdentity("Test User");
var httpContext = new DefaultHttpContext()
{
    User = new GenericPrincipal(identity, null)
};

var context = new ControllerContext { HttpContext = httpContext};
builder.RegisterInstance(context);

다음으로 컨트롤러를 등록 할 때 속성 주입을 활성화합니다.

  builder.RegisterAssemblyTypes(assembly)
                    .Where(t => t.Name.EndsWith("Controller")).PropertiesAutowired();

그런 다음 User.Identity.Name채워지고 컨트롤러에서 메서드를 호출 할 때 특별한 작업을 수행 할 필요가 없습니다.

public async Task<ActionResult<IEnumerable<Employee>>> Get()
{
    var requestedBy = User.Identity?.Name;
    ..................


답변