gerryLowry
gerryLowry

Reputation: 2656

How to xUnit test against AccountController's actual ASP.NET Core 1.0 Identity tables without mocking?

There are more than a few articles about unit testing against the ASP.NET Core 1.0 AccountController.

It seems that none of them actually involve testing the contents of the real Asp.Net Core 1.0 Identity tables.

There are 7 tables:

AspNetUsers
AspNetUserTokens
AspNetUserLogins
AspNetUserRoles
AspNetUserClaims
AspNetRoles
AspNetRoleClaims

in a .NET Core Web API application, the flow (except for external code framework parts we do not see) is first through Main

    public static void Main(string[] args)
    {
        var host = new WebHostBuilder()
            .UseKestrel()
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseIISIntegration()
            .UseStartup<Startup>()
            .Build();

        host.Run();
    }

From var host = ... we enter public Startup(IHostingEnvironment env) in Startup.cs; after that the runtime method public void ConfigureServices(IServiceCollection services) allows configuration:

        // Add framework services.
        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

        services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();

        services.AddMvc();

et cetera

Finally host.Run(); puts our .NET Core Web API application into a listening mode.

http://localhost:58796/account/login causes a Log in form to be returned.

The constructor for the AccountController looks like this:

    public AccountController(
        UserManager<ApplicationUser> userManager,
        SignInManager<ApplicationUser> signInManager,
        IEmailSender emailSender,
        ISmsSender smsSender,
        ILoggerFactory loggerFactory)
    {
        _userManager = userManager;
        _signInManager = signInManager;
        _emailSender = emailSender;
        _smsSender = smsSender;
        _logger = loggerFactory.CreateLogger<AccountController>();
    }

This means that a UserManager and a SignInManager must be passed in to the AccountController; the SignInManager can not be null and requires a non-null UserManager and a non-null logger:

   public SignInManager(UserManager<TUser> userManager,
       IHttpContextAccessor contextAccessor,
       IUserClaimsPrincipalFactory<TUser> claimsFactory,
       IOptions<IdentityOptions> optionsAccessor, 
       ILogger<SignInManager<TUser>> logger);

Setting up the logger is relatively simple in our xUnit.net method:

        ILoggerFactory loggerFactory = new LoggerFactory()
                                      .AddConsole()
                                      .AddDebug();

Setting up the userManager is more complicated in our xUnit.net method:

    public UserManager(IUserStore<TUser> store,
            IOptions<IdentityOptions> optionsAccessor,
            IPasswordHasher<TUser> passwordHasher,
            IEnumerable<IUserValidator<TUser>> userValidators, 
            IEnumerable<IPasswordValidator<TUser>> passwordValidators, 
            ILookupNormalizer keyNormalizer,
            IdentityErrorDescriber errors,
            IServiceProvider services,
            ILogger<UserManager<TUser>> logger);

Part of the challenge is the complexity of implementing the interface IUserStore<TUser> contract for the store:

// Summary:  Provides an abstraction for a store which manages user accounts.
//   TUser:  The type encapsulating a user.
public interface IUserStore<TUser> : IDisposable where TUser : class
{
    Task<IdentityResult> CreateAsync(TUser user, CancellationToken cancellationToken);
    Task<IdentityResult> DeleteAsync(TUser user, CancellationToken cancellationToken);
    Task<TUser> FindByIdAsync(string userId, CancellationToken cancellationToken);
    Task<TUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken);
    Task<string> GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken);
    Task<string> GetUserIdAsync(TUser user, CancellationToken cancellationToken);
    Task<string> GetUserNameAsync(TUser user, CancellationToken cancellationToken);
    Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken);
    Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken);
    Task<IdentityResult> UpdateAsync(TUser user, CancellationToken cancellationToken);
}

For the normal use of the AccountController much of the complexity is buried behind the scenes by the ASP.NET Core 1.0 Web API wiring which takes care of providing access to the Identity database tables via EF Core 1.0.

For unit testing with xUnit.net without mocking (because we want to access the "real database"), the problem is that the flow is like this:

 [Fact]
 // Arrange
     ...
 // Act
     ... // the AccountController is not available yet
 var accountController
     = new AccountController(userManager: ...,
              signInManager: ...,
                emailSender: null,
                smsSender: null,
                loggerFactory: loggerFactory); // can NOT be null
.............

in a nutshell, the problem is a catch 22 ~~ Start.cs sets up everything we need, however, our xUnit.net test is running before Start.cs has been run.

a .zip can be found here github.com/gerryLowry/RawCoreAPIxUnit

a not zipped version can be found here github.com/gerryLowry/EF_Core_testing_experiments

Upvotes: 2

Views: 1196

Answers (1)

yohanmishkin
yohanmishkin

Reputation: 168

Maybe give Microsoft.AspNetCore.TestHost (ships with Asp.Net Core for integration testing) a go.

You can initialize the TestServer in the constructor of your test class and wire up your custom Startup.cs with it. Then register mock implementations of all of AccountController's dependencies except for your one real database dependency.

For the "real database" construct UserManager with UserStore and Entity Framework implementation of IUserStore (link)

Upvotes: 2

Related Questions