Reputation: 505
Background:
I am working on getting some experience in unit testing, as per my new employer's strict unit testing requirements, but unit testing as a whole is new to me. I have had a TON of issues trying to test any of the methods that make use of ASP.NET Identity, due to the reliance on
HttpContext.Current.GetOwinContext().Authentication
and
HttpContext.Current.GetOwinContext().GetUserManager
Now, this current project has not fully taken advantage of Interfaces and Dependency Injection so far, but as a part of incorporating our organization's Active Directory system into our Identity Database (creating a row in the Users table for someone when they log in with valid Active Directory credentials, and then using the Identity database for them from then on), I have been working on retrofitting our Identity side of things with Interfaces and Ninject, and adding FakeItEasy to our test unit test projects.
This does mean that we are currently generating tests that use the actual databases themselves as opposed to faking things. We know this is a bad practice and it was done simply for the sake of not overloading us new guys' minds while we work on our first real project. We have worked out (most/all) of the kinks this causes and our tests clean things up when they are finished.
Question:
I am running into a peculiar issue while trying to unit test the following method:
public bool ResetPassword(User user)
{
if (!user.EmailConfirmed) return false;
user.RequirePasswordReset = true;
string randomGeneratedPassword = GenerateRandomPassword(20); // defined at end of class
user.PasswordHash = hash.HashPassword(randomGeneratedPassword);
if (!UpdateUser(user)) return false;
string message = "We have received a request to reset your password. <br /><br />" +
"Your new password is shown below. <br /><br />" +
$"Your new password is: <br />{randomGeneratedPassword}<br /><br />" +
"This password is only valid for one login, and must be changed once it is used. <br /><br /><br /><br />" +
"Server<br />AmTrust Developer University";
SendFormattedEmail(user.Email, user.FullName, message, "Your password has been reset");
return true;
}
The test I have written so far to do so (with the testcases omitted) is:
public bool ResetPasswordTests(bool emailConfirmed)
{
//arrange
_user = new User()
{
Email = "[email protected]",
EmailConfirmed = emailConfirmed,
FirstName = "Test",
isActive = true,
isActiveDirectoryAccount = false,
LastName = "Testingly",
PasswordHash = _hash.HashPassword("secret1$"),
RequirePasswordReset = false,
UserName = "ttestingly"
};
string hashedPass = _user.PasswordHash;
_identityContext.Users.Add(_user);
_identityContext.SaveChanges();
//Suppress the email sending bit!
A.CallTo(() => _userBusinessLogic_Testable.SendFormattedEmail(null, null, null, null, null))
.WithAnyArguments()
.DoesNothing();
//act
bool result = _userBusinessLogic_Testable.ResetPassword(_user);
//assert
Assert.That(result);
Assert.That(_user.PasswordHash != hashedPass);
Assert.That(_user.RequirePasswordReset);
return result;
}
Running this test (for all of its various TestCases) returns the following exception:
System.NotSupportedException: Model compatibility cannot be checked because the database does not contain model metadata. Model compatibility can only be checked for databases created using Code First or Code First Migrations.
This is caused by _identityContext.Users.Add(_user);
Everything I've seen about this issue indicates that it is caused by an open connection to the database while code is run trying to connect to that database, which I don't think is the case, or from trying to have EF manage a pre-existing database (which is not the case: I have deleted my databases multiple times between tests to try to verify this).
Note: Currently all of our team's databases are just localhost databases, so there's no one else messing with my stuff.
I have seen an example of this where the solution was to change the connection string, however this issue ONLY happens in Unit Testing - I have verified that while running, everything on the application works as expected prior to any changes I have made to incorporate Interfaces, Ninject, and Active Directory - so I do not think the connection string itself is the issue, but here is the relevant connection string
(they are proper XML but I'm not sure how to get them to show up properly on Stack Overflow, so I removed all of the braces):
connectionStrings
add name="ADUUserDB" providerName="System.Data.SqlClient" connectionString="Data Source=localhost\sql2014;Initial Catalog=ADUUserDB;Integrated Security=True;Connect Timeout=15;Encrypt=False;TrustServerCertificate=False; MultipleActiveResultSets=True"
/connectionStrings
Upvotes: 2
Views: 207
Reputation: 505
I figured out what this issue was, and embarrassingly it was a simple configuration issue - I didn't actually fake the Repository that was being used by the BLL I was attempting to test. I was skipping straight to the database context(s), which worked previously but for some reason was failing with the new, partially-faked setup I had at the time.
So, the changes I had to make were simple:
private IUserRepository _userRepository = A.Fake<IUserRepository>();
with the relevant underlying repository calls being faked with A.CallTo();
This also removes the need to add things to the database and then remove them at the end of the test, which was where the error was thrown anyway.
Upvotes: 2
Reputation: 241714
I've no information about the database stuff, but think I have an idea for the A.CallTo
. FakeItEasy is unhappy at being asked to configure Single
, which is an extension method. You should just be configuring the Fake. Perhaps
A.CallTo(() => _userBusinessLogic.GetUsers(null, null, null, null, null, null, null))
.WithAnyArguments()
.Returns(new [] { _user });
To be fair, FakeItEasy could do a better job of pointing you at the problem (although usually this comes up when there's only one method call inside the A.CallTo
, so it's a little easier to tell). I've created issue 786 to track this.
Upvotes: 2