Simple UI testing is easy enough in ASP.NET MVC. Essentially all you have to do is assert that the returned HTML contains the elements you need. While this ensures that the HTML page is structured the way you expect, it doesn't fully test the UI.
Proper web UI testing requires a tool like Selenium that will use browsers on your machine and ensure that the JavaScript and HTML are working properly in all browsers. Selenium does have a client/server model so that you can have a set of virtual machines with Unix, Mac, and Windows clients and the set of browsers common to those environements.
Now, a well designed MVC (pattern, not framework) application puts the important logic in the models and controllers. In short, the functionality of the application is tested when you test those two aspects. Views tend to only have display logic and are easily checked with visual inspection. Due to the thin processing in the view and the bulk of the application being well tested, many people don't think that the pain of testing the view layer outweighs the benefit gained by it.
That said, MVC does have some nice facilities to check the DOM returned by the request. That reduces the pain quite a bit for testing the view layer.
Unit tests, if you're testing small enough units, are always asserting the blindingly obvious.
The reason that add(x, y)
even gets mention of a unit test, is because sometime later somebody will go into add
and put special tax logic handling code not realizing that add is used everywhere.
Unit tests are very much about the associative principle: if A does B, and B does C, then A does C. "A does C" is a higher-level test. For instance, consider the following, completely legitimate business code:
public void LoginUser (string username, string password) {
var user = db.FetchUser (username);
if (user.Password != password)
throw new Exception ("invalid password");
var roles = db.FetchRoles (user);
if (! roles.Contains ("member"))
throw new Exception ("not a member");
Session["user"] = user;
}
At first glance this looks like an awesome method to unit test, because it has a very clear purpose. However, it does about 5 different things. Each thing has a valid and invalid case, and will make a huge permutation of unit tests. Ideally this is broken down further:
public void LoginUser (string username, string password) {
var user = _userRepo.FetchValidUser (username, password);
_rolesRepo.CheckUserForRole (user, "member");
_localStorage.StoreValue ("user", user);
}
Now we're down to units. One unit test does not care what _userRepo
considers valid behavior for FetchValidUser
, only that it's called. You can use another test to ensure exactly what a valid user constitutes. Similarly for CheckUserForRole
... you've decoupled your test from knowing what the Role structure looks like. You've also decoupled your entire program from being tied strictly to Session
. I imagine all the missing pieces here would look like:
class UserRepository : IUserRepository
{
public User FetchValidUser (string username, string password)
{
var user = db.FetchUser (username);
if (user.Password != password)
throw new Exception ("invalid password");
return user;
}
}
class RoleRepository : IRoleRepository
{
public void CheckUserForRole (User user, string role)
{
var roles = db.FetchRoles (user);
if (! roles.Contains (role))
throw new Exception ("not a member");
}
}
class SessionStorage : ILocalStorage
{
public void StoreValue (string key, object value)
{
Session[key] = value;
}
}
By refactoring you have accomplished several things at once. The program is way more supportive of tearing out underlying structures (you can ditch the database layer for NoSQL), or seamlessly adding locking once you realize Session
isn't thread-safe or whatever. You've also now given yourself very straightfoward tests to write for these three dependencies.
Hope this helps :)
Best Answer
One of the aims of unit testing is to test just one thing at a time i.e. a single class and method. If the repository itself is not under test then you would normally mock this out in some way so as just to test the logic in your method/class.
That said you do also need to test with a 'real' repository*, but this would normally be done in an integration/system test
*obviously real as in repo set up for test, hopefully not e.g. the production DB.