My coworker and I were discussing the correct way to write a unit test to make sure a user receives an error when entering bad data into a form in our ASP.NET MVC 2 application. Here's what we've done in the past, starting with the model:
public class LoginModel
{
public string Username { get; set; }
}
Here's the controller action:
[HttpPost]
public ActionResult Login( LoginModel loginModel )
{
if ( loginModel.Username == null )
{
ModelState.AddModelError( "Username", "Username is required!" );
return View( loginModel );
}
LoginService.Login( loginModel );
}
Finally, here's the test method:
[TestMethod]
public void Login_Post_Blank_Username_Displays_Error()
{
var controller = GetHomeController();
var loginModel = new LoginModel
{
Username = null
};
var result = controller.Login( loginModel );
Assert.IsInstanceOfType( result, typeof( ViewResult ) );
var view = (ViewResult)result;
Assert.IsNotNull( view.ViewData.ModelState["Username"].Errors.First().ErrorMessage );
}
He pointed out to me that this is really not the correct way to write a test for this situation. For one reason, it's extremely brittle - changing the Username property to anything else will break the test. Second, it would be better to rely on DataAnnotations and just test against what the controller is doing. So, our new model would look like this:
public class LoginModel
{
[Required( ErrorMessage = "Username is required!" )]
public string Username { get; set; }
}
And our controller action would change to this:
[HttpPost]
public ActionResult Login( LoginModel loginModel )
{
if ( !Model.IsValid() )
{
return View( loginModel );
}
LoginService.Login( loginModel );
}
The problem comes with the unit test, which is totally oblivious to DataAnnotations, so the test fails. My coworker said that what we should REALLY be testing is that the LoginService is NOT called, but I'm not sure how to test for this. He suggested using Moq like this:
[TestMethod]
public void Login_Post_Blank_Username_Displays_Error()
{
var controller = GetHomeController();
var loginModel = new LoginModel
{
Username = null
};
loginServiceMock.Setup( x => x.Login( It.IsAny<LoginModel>() ) )
.Callback( () => Assert.Fail( "Should not call LoginService if Username is blank!" ) );
var result = controller.Login( loginModel );
loginServiceMock.Verify();
}
What are your thoughts on this? What's the correct way to test that a service method was NOT called? What about the correct way to test for bad data in a user form?
When verifying:
loginServiceMock.Verify( x => x.Login( It.IsAny<LoginModel>() ), Times.Never() );
You should remove the setup in the mock.
In order to make the code enter the if statement in your action, you can add a model error in the ModelState before invoking the action in the controller.
Here's how your code should look like:
[TestMethod]
public void Login_Post_Blank_Username_Displays_Error()
{
var controller = GetHomeController();
var loginModel = new LoginModel
{
Username = null
};
controller.ModelState.AddModelError("a key", "a value");
var result = controller.Login( loginModel );
loginServiceMock.Verify( x => x.Login( It.IsAny<LoginModel>() ), Times.Never() );
}
Assuming that your Mocked LoginService has been set via property injection, you can write the test as below..
[TestMethod]
public void LoginPost_WhenUserNameIsNull_VerifyLoginMethodHasNotBeenCalled()
{
//Arrange
var loginServiceMock = new Mock<ILoginService>();
var sut = new HomeController { LoginService = loginServiceMock .Object};
var loginModel = new LoginModel {
Username = null
};
sut.ModelState.AddModelError("fakeKey", "fakeValue");
//Act
sut.Login(loginModel);
//Verify
loginServiceMock.Verify(x => x.Login(loginModel), Times.Never(), "Login method has been called.");
}
Related
I have a controller in which I am unit testing my Index action. I am having problem in unit testing User.Identity.GetUserId()
This is my controller
public ActionResult Index()
{
string userId = User.Identity.GetUserId();
DemoModel demoModel = _demoModelService.GetByUserId(userId);
MyModel myModel = new MyModel()
{
Name = demoModel.Name;
Description = demoModel.Description;
}
return View(myModel);
}
This is my Unit Test:
public void Test_Index_Action()
{
//Act
var result = controller.Index() as ViewResult;
//Assert
Assert.AreEqual("", result.ViewName);
}
When I debug my test method, as it reaches the first line of code(User.Identity.GetUserId) of my Index action, it generates null UserId. How can I access the userId in unit testing this code?
I've been struggeling with mvc unit test my self, while there are known techniques to improve testability of your mvc application, most of the projects I worked on are sadly not following them.
So I decided to start this project to help me and others who love to unit test their mvc application. Please take a look at: https://github.com/ibrahimbensalah/Xania.AspNet.Simulator.
Here is an example unit test class
using System.Web.Mvc;
using NUnit.Framework;
using Xania.AspNet.Simulator;
public class SimulatorTests
{
[Test]
public void ActionExecuteTest()
{
// arange
var controller = new TestController();
// act
var result = controller.Execute(c => c.Index());
// assert
Assert.AreEqual("Hello Simulator!", result.ViewBag.Title);
}
[Test]
public void UnAuthorizedActionTest()
{
// arrange
var controller = new TestController();
// act
var result = controller.Execute(c => c.UserProfile());
// assert
Assert.IsInstanceOf<HttpUnauthorizedResult>(result.ActionResult);
}
[Test]
public void AuthorizedActionTest()
{
// arrange
var controller = new TestController();
// act
var result = controller.Action(c => c.UserProfile()).Authenticate("user", null).Execute();
// assert
Assert.IsInstanceOf<ViewResult>(result.ActionResult);
}
}
public class TestController : Controller
{
public ActionResult Index()
{
ViewBag.Title = "Hello Simulator!";
return View();
}
[Authorize]
public ActionResult UserProfile()
{
return View();
}
}
Has anyone had a problem with this. He throws me the error: Name can not be null or empty. But in the table do not have the attribute called "Name". I want to customize Asp.Net Identity.
Controller
public class HomeController : Controller
{
//
// GET: /Home/
public async Task<ActionResult> Index()
{
var userStore = new UserStore<User,Identity_2Context.MyRole,long,Identity_2Context.MyUserLogin,Identity_2Context.MyUserRole,Identity_2Context.MyClaim>(new Identity_2Context());
var manager = new UserManager<User,long>(userStore);
var user = new User()
{
UserName = "TehnoMac",Email = "TehnoMac#tehcno.com",IsApproved = true,IsLockedOut = false,
CreatedDate = DateTime.Now,LastActivityDate = DateTime.Now,LastLoginDate = DateTime.Now,FailedPasswordAnswerAttemptCount = 0,
FailedPasswordAttemptCount = 0
};
var result = await manager.CreateAsync(user, "test123test");
if (result.Succeeded)
{
var authenticationManager = HttpContext.GetOwinContext().Authentication;
var userIdentity = manager.Create(user, DefaultAuthenticationTypes.ApplicationCookie);
}
else
{
ViewBag.Text = result.Errors.FirstOrDefault();
}
return View();
}
}
IdentityModel
public class ApplicationUser : IdentityUser
{
}
public class MyClaim:IdentityUserClaim<long>
{
}
public class MyRole:IdentityRole<long,MyUserRole>
{
}
public class MyUserRole:IdentityUserRole<long>
{
}
public class MyUserLogin:IdentityUserLogin<long>
{
}
public class ApplicationDbContext : IdentityDbContext<ApplicationUser,MyRole,long,MyUserLogin,MyUserRole,MyClaim>
{
public ApplicationDbContext()
: base("DefaultConnection")
{
}
}
Make sure you have specified a username. I got the same error and after setting the UserName property, the error went away.
Solved (for me at least) By REMOVING the UserName property from the customized identity (its in the GitHub ASPNetIdentitySample project which is where I guess people are getting it from). Its hiding the UserName property in IdentityUser
You have to specify a UserName for identities even if you don`t use it. For example you need to log in with email and password than you can set UserName with email variable.
passing email variable to UserName property
Why we need to do that ? Because we need a UserName in IdentityUser constructor.
IdentityUser constructor
I can't find any reason for this unit test to fail, but it does every time.
HomeController.cs:
[HandleError]
public class HomeController : Controller
{
public ActionResult Index()
{
var model = "Whatever";
return View( model );
}
}
HomeControllerTest.cs:
[TestClass]
public class HomeControllerTest
{
[TestMethod]
public void Index()
{
var controller = new HomeController();
var result = controller.Index() as ViewResult;
var model = result.ViewData.Model;
Assert.IsInstanceOfType( model, typeof(string) );
}
}
result is not null, nor is result.ViewData. But result.ViewData.Model is always null.
What in the world is causing this to fail? It's such a simple test...
EDIT
This is even weirder. If I create a class, and use the class as the model, it doesn't fail. It only fails when the model is a string! i.e., this passes:
HomeController.cs:
[HandleError]
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new SomeClass
{
Name = "Whatever"
};
return View( model );
}
}
public class SomeClass
{
public string Name { get; set; }
}
HomeControllerTest.cs
[TestClass]
public class HomeControllerTest
{
[TestMethod]
public void Index()
{
var controller = new HomeController();
var result = controller.Index() as ViewResult;
var model = result.ViewData.Model;
Assert.IsInstanceOfType( model, typeof(SomeClass) );
}
}
The model is null that's why.
When you pass a string as the argument to View(string), the string is actually the view name, not a model.
Cast it as an object to use the overload for the model.
return View((object)model);
View(Object) - Creates a ViewResult object by using the model that renders a view to the response.
View(String) - Creates a ViewResult object by using the view name that renders a view.
There is a simple controller that a querystring is read in constructor of it.
public class ProductController : Controller
{
parivate string productName;
public ProductController()
{
productName = Request.QueryString["productname"];
}
public ActionResult Index()
{
ViewData["Message"] = productName;
return View();
}
}
Also I have a function in unit test that create an instance of this Controller and I fill the querystring by a Mock object like below.
[TestClass]
public class ProductControllerTest
{
[TestMethod]
public void test()
{
// Arrange
var querystring = new System.Collections.Specialized.NameValueCollection { { "productname", "sampleproduct"} };
var mock = new Mock<ControllerContext>();
mock.SetupGet(p => p.HttpContext.Request.QueryString).Returns(querystring);
var controller = new ProductController();
controller.ControllerContext = mock.Object;
// Act
var result = controller.Index() as ViewResult;
// Assert
Assert.AreEqual("Index", result.ViewName);
}
}
Unfortunately Request.QueryString["productname"] is null in constructor of ProductController when I run test unit.
Is ther any way to fill a querystrin by a mocking and get it in constructor of a control?
There is a simple controller that a querystring is read in constructor of it.
You shouldn't be doing this and such controller shouldn't exist. The controller context is not yet initialized in the constructor and it will fail not only for the unit test but in real.
You should use the Initialize method where you have access to the request context.
I have been working my way through Scott Guthrie's excellent post on ASP.NET MVC Beta 1. In it he shows the improvements made to the UpdateModel method and how they improve unit testing. I have recreated a similar project however anytime I run a UnitTest that contains a call to UpdateModel I receive an ArgumentNullException naming the controllerContext parameter.
Here's the relevant bits, starting with my model:
public class Country {
public Int32 ID { get; set; }
public String Name { get; set; }
public String Iso3166 { get; set; }
}
The controller action:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(Int32 id, FormCollection form)
{
using ( ModelBindingDataContext db = new ModelBindingDataContext() ) {
Country country = db.Countries.Where(c => c.CountryID == id).SingleOrDefault();
try {
UpdateModel(country, form);
db.SubmitChanges();
return RedirectToAction("Index");
}
catch {
return View(country);
}
}
}
And finally my unit test that's failing:
[TestMethod]
public void Edit()
{
CountryController controller = new CountryController();
FormCollection form = new FormCollection();
form.Add("Name", "Canada");
form.Add("Iso3166", "CA");
var result = controller.Edit(2 /*Canada*/, form) as RedirectToRouteResult;
Assert.IsNotNull(result, "Expected to be redirected on successful POST.");
Assert.AreEqual("Show", result.RouteName, "Expected to redirect to the View action.");
}
ArgumentNullException is thrown by the call to UpdateModel with the message "Value cannot be null. Parameter name: controllerContext". I'm assuming that somewhere the UpdateModel requires the System.Web.Mvc.ControllerContext which isn't present during execution of the test.
I'm also assuming that I'm doing something wrong somewhere and just need to pointed in the right direction.
Help Please!
I don't think it can be done since TryUpdateModel, which UpdateModel uses, references the ControllerContext which is null when invoked from a unit test. I use RhinoMocks to mock or stub the various components needed by the controller.
var routeData = new RouteData();
var httpContext = MockRepository.GenerateStub<HttpContextBase>();
FormCollection formParameters = new FormCollection();
EventController controller = new EventController();
ControllerContext controllerContext =
MockRepository.GenerateStub<ControllerContext>( httpContext,
routeData,
controller );
controller.ControllerContext = controllerContext;
ViewResult result = controller.Create( formParameters ) as ViewResult;
Assert.AreEqual( "Event", result.Values["controller"] );
Assert.AreEqual( "Show", result.Values["action"] );
Assert.AreEqual( 0, result.Values["id"] );
Here's the relevant bit from the Controller.cs source on www.codeplex.com/aspnet:
protected internal bool TryUpdateModel<TModel>( ... ) where TModel : class
{
....
ModelBindingContext bindingContext =
new ModelBindingContext( ControllerContext,
valueProvider,
typeof(TModel),
prefix,
() => model,
ModelState,
propertyFilter );
...
}
I was having this same issue. After reading tvanfosson's solution, I tried a simple solution not involving a mock framework.
Add a default ControllerContext to the controller as follows:
CountryController controller = new CountryController();
controller.ControllerContext = new ControllerContext();
This removed the error just fine for me while unit testing. I hope this may help someone else out.
Or you can create form data proxy, like
public class CountryEdit {
public String Name { get; set; }
public String Iso3166 { get; set; }
}
Plus. Easy create unit tests
Plus. Define white list of fields update from post
Plus. Easy setup validation rules, easy test it.
Minus. You should move date from proxy to you model
So Controller.Action should look, like
public ActionResult Edit(Int32 id, CountryEdit input)
{
var Country = input.ToDb();
// Continue your code
}