ASP.NETCoreでのIPrincipalのモック


89

単体テストを作成しているASP.NETMVCCoreアプリケーションがあります。アクションメソッドの1つは、一部の機能にユーザー名を使用します。

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

これは明らかにユニットテストで失敗します。私は周りを見回しましたが、すべての提案は.NET4.5からモックHttpContextまでです。それを行うためのより良い方法があると確信しています。IPrincipalを注入しようとしましたが、エラーが発生しました。そして私もこれを試しました(必死になって、私は推測します):

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

しかし、これもエラーをスローしました。ドキュメントにも何も見つかりませんでした...

回答:


179

コントローラのがアクセスされるを通してコントローラ。後者内に保存されます。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が正しく機能することが確認されます(コードでこれを使用して、ユーザーが認証されているかどうかを判断する場合)。


7
私の場合、new Claim(ClaimTypes.Name, "1")コントローラーの使用と一致させることでしたuser.Identity.Name。しかし、そうでなければ、それはまさに私が達成しようとしていたことです...ダンケ・ション!
フェリックス

数え切れないほどの時間を検索した後、これは最終的に私を二乗させた投稿でした。コア2.0プロジェクトコントローラーメソッドではUser.FindFirstValue(ClaimTypes.NameIdentifier);、作成中のオブジェクトにuserIdを設定するために使用していましたが、プリンシパルがnullであるために失敗していました。これは私のためにそれを修正しました。素晴らしい答えをありがとう!
ティモシーランドール2017

また、UserManager.GetUserAsyncを機能させるために数え切れないほどの時間を検索していましたが、これが欠落しているリンクを見つけた唯一の場所です。ありがとう!GenericIdentityを使用するのではなく、Claimを含むClaimsIdentityを設定する必要があります。
エティエンヌチャーランド

17

以前のバージョン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;

これら2つは読み取り専用のプロパティであることがわかります。良いニュースは、ControllerContextプロパティがその値を設定できるので、それがあなたの道になるということです。

したがって、目標はそのオブジェクトに到達することです。In CoreHttpContextは抽象的であるため、モックするのははるかに簡単です。

のようなコントローラーを想定

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);
}

3

既存のクラスを使用して、必要な場合にのみモックする可能性もあります。

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

3

私の場合、私はを利用するために必要なRequest.HttpContext.User.Identity.IsAuthenticatedRequest.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();

2

抽象ファクトリパターンの実装を検討します。

特にユーザー名を提供するためのファクトリのインターフェイスを作成します。

次に、具体的なクラスを提供します。1つはを提供しUser.Identity.Name、もう1つはテストで機能する他のハードコードされた値を提供します。

その後、本番コードとテストコードに応じて、適切な具象クラスを使用できます。おそらく、ファクトリをパラメータとして渡すか、構成値に基づいて正しいファクトリに切り替えることを検討しています。

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());

ありがとうございました。私は自分のオブジェクトに対して同様のことをしています。IPrinicpalのような一般的なものには、「箱から出してすぐに使える」何かがあることを望んでいました。しかし、どうやら、そうではありません!
フェリックス

さらに、UserはControllerBaseのメンバー変数です。そのため、以前のバージョンのASP.NETでは、人々はHttpContextをモックし、そこからIPrincipalを取得していました。ProductionFactoryのようなスタンドアロンクラスからユーザーを取得することはできません
Felix

1

コントローラを直接ヒットして、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;
    ..................
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.