JustMock is a great tool for abstracting dependencies in unit tests, and the new automocking feature makes it even faster to develop unit tests. Another great feature in JustMock and JustMock Lite is the capability to assert the behavior of your system under test.
Traditional TDD (Test Driven Testing) unit testing typically tests for state. Did the user get logged in? Did the user’s shopping cart get loaded? Important tests, of course. But that only tests the end result of the method. If the user does NOT successfully login, and the cart is not reloaded, is that because the call to the repository was never called? Or because some error happened that didn’t reload the cart in this particular use case? The state of the application is correct, but is that because it executed the expected behavior, or because we got lucky?
In addition to state based testing, another set of tests need to assert the behavior of the system under test and how it relates to its dependencies. If a user failed to login, the cart repository should never be called. Fortunately, with JustMock and JustMock Lite, this is very easy to accomplish!
The code that we will be testing is shown in Listing 1. In the project, I have all of the classes in separate files, but for ease of blogging, I included them all in one listing.
using
System;
namespace
IntroToMocking.AssertingBehavior
{
public
class
SecurityHandler
{
private
readonly
ILoginService _service;
private
readonly
IShoppingCartRepository _cartRepo;
private
readonly
ISecurityLogger _logger;
public
Cart ShoppingCart {
get
;
internal
set
; }
public
int
UserID {
get
;
internal
set
; }
public
SecurityHandler(
ILoginService service,
IShoppingCartRepository cartRepo,
ISecurityLogger logger)
{
_logger = logger;
_cartRepo = cartRepo;
_service = service;
}
public
bool
LoginUser(
string
userName,
string
password)
{
UserID = _service.ValidateUser(userName, password);
if
(UserID != 0)
{
ShoppingCart = _cartRepo.LoadCart(UserID);
return
true
;
}
else
{
_logger.LogInvalidLogin(userName, password);
}
return
false
;
}
}
public
interface
IShoppingCartRepository
{
Cart LoadCart(
int
userID);
}
public
interface
ISecurityLogger
{
void
LogInvalidLogin(
string
userName,
string
password);
}
public
interface
ILoginService
{
int
ValidateUser(
string
userName,
string
password);
}
public
class
Cart
{
}
}
There are two behaviors that we want to assert. The first is that the cart repository only attempts to reload the cart if the user successfully logs in. The second is that the logger should only log an invalid login attempt when there is an invalid login (ok, that one seems so obvious now that I’ve typed it).
JustMock allows you to specify the number of times a mock method should be called in a variety of ways, as listed in Table 1. In addition to specifying the occurrences, Assert() must be called on the mock. By convention, this is done at the end of the test, after the other assert.
Specification | |
MustBeCalled() | Call must be executed at least one time |
Occurs(x) | Call must be executed exactly x number of times |
OccursOnce() | Call must be executed one time only (same as Occurs(1)) |
OccursNever() | Call must not be executed (same as Occurs(0)) |
OccursAtLeast(x) | Call must be executed at least x number of times |
OccursAtMost(x) | Call can be executed at most x number of times |
Table 1 – Specifying Occurrences of method calls
The behavior that we expect from the system under test is to not call the cart repository when the login attempt fails. This is done very simply by adding .Occurs(0) (or .OccursNever()) to the end of the arrange for the cart repository. I’ve also added .Occurs(1) to the arrange for the ILoginService to verify that the system under test actually calls into the service to validate the user credentials. The test is shown in Listing 2. NOTE: When using automocking, calling Assert() on the container verifies all of the specifications for all of the mocks created by the automocking container. If I didn’t use automocking in this test, I would have to specifically create a mock for all three dependencies, including the Logger that isn’t pertinent to this test. I would then have to call Assert on the mocks for the ILoginService and the IShoppingCartRepository.
[Test]
public
void
Should_Not_Load_Cart_With_Invalid_Username_And_Password_Automocked()
{
string
userName =
"Bob"
;
string
password =
"Password"
;
int
userID = 0;
Cart cart =
new
Cart();
var container =
new
MockingContainer<SecurityHandler>();
container.Arrange<ILoginService>(x =>
x.ValidateUser(userName, password))
.Returns(userID)
.Occurs(1);
container.Arrange<IShoppingCartRepository>(x =>
x.LoadCart(userID))
.Occurs(0);
bool
result = container.Instance.LoginUser(
userName, password);
Assert.IsFalse(result);
Assert.AreEqual(container.Instance.UserID, userID);
container.Assert();
}
The final step in JustMock’s behavior testing is the ability to specify the order of execution for dependencies. The ILogger logs invalid login attempts. Logging an invalid login shouldn’t be executed until after the login actually fails. To test for this is easy – simply add InOrder() to the arrangements. The order is specified as the same order that the arrangements are listed in the test, and works across different mock objects. The test for the behavior of Logging is shown in Listing 3.
[Test]
public
void
Should_Log_Invalid_Login_Attempts()
{
string
userName =
"Bob"
;
string
password =
"Password"
;
int
userID = 0;
Cart cart =
new
Cart();
var container =
new
MockingContainer<SecurityHandler>();
container.Arrange<ILoginService>(x =>
x.ValidateUser(userName, password))
.Returns(userID)
.InOrder()
.Occurs(1);
container.Arrange<ISecurityLogger>(x =>
x.LogInvalidLogin(userName, password))
.InOrder()
.Occurs(1);
bool
result = container.Instance.LoginUser(userName, password);
Assert.IsFalse(result);
Assert.AreEqual(container.Instance.UserID, userID);
container.Assert();
}
Asserting behavior is often overlooked when developers are testing their code, but automating the testing of code interaction is an extremely valuable exercise. Not every test should include behavior checking, but there should be enough coverage to ensure the system under test is interacting with the dependencies as expected.
Happy Coding!
Philip Japikse is an international speaker, a Microsoft MVP, ASPInsider, INETA Community Champion, MCSD, CSM/ CSP, and a passionate member of the developer community. Phil has been working with .Net since the first betas, developing software for over 20 years, and heavily involved in the agile community since 2005. Phil also hosts the Hallway Conversations podcast (www.hallwayconversations.com) and serves as the Lead Director for the Cincinnati .Net User’s Group (http://www.cinnug.org). You can follow Phil on twitter via www.twitter.com/skimedic, or read his personal blog at www.skimedic.com/blog.