Port of schuchert.wikispaces.com


Mockito.LoginServiceExample

Getting Started

I’m assuming you can download Mockito and get it in your classpath. So I’ll start with tests that implement some of the requirements from here.

However, in a nutshell:

Writing The Tests

What follows is a series of tests to get enough production code written to suggest a better implementation. The first purpose of this tutorial is to demonstrate using Mockito for all types other than the underling LoginService. This is close to a classic mockist approach, though it varies in that I’m emphasizing testing interaction rather than state and deliberately trying to write stable tests that do not depend too much on the underling implementation. In support of this:

Test 1: Basic Happy Path

When a user logs in successfully with a valid account id and password, the account’s state is set to logged in. Here’s a way to test that:

package com.om.example.loginservice;

import org.junit.Test;
import static org.mockito.Mockito.*;

public class LoginServiceTest {

   @Test
   public void itShouldSetAccountToLoggedInWhenPasswordMatches() {
      IAccount account = mock(IAccount.class);
      when(account.passwordMatches(anyString())).thenReturn(true);

      IAccountRepository accountRepository = mock(IAccountRepository.class);
      when(accountRepository.find(anyString())).thenReturn(account);
      
      LoginService service = new LoginService(accountRepository);
      
      service.login("brett", "password");
      
      verify(account, times(1)).setLoggedIn(true);
   }
}

Test Description

Part 1 This test first creates a test-double for an IAccount. There’s no actual account class, just the interface. This test-double is configured so that no matter what password is sent to it, it will always return true when asked if a provided password matches its password.
Part 2 Create a test-double for an IAccountRepository. Associate the test-double IAccount with the test-double IAccountRepository. When asking for any account with an id equal to any string, return the account test-double created at the start of this method.
Part 3 Create a LoginService, injecting the IAcccountRepsitory in the constructor. This is an example of Inversion of Control, rather than the LoginService knowing which IAccountRepository to talk to, it is told which one to talk to. So while the LoginService knows which messages to send to an IAccountRepository, it is not responsible for deciding towhich instance it should send messages.
Part 4 Actually send a login message, looking for account with id “brett” and a password of “password”. Notice that if things are configured correctly, any account id will match as will any password.
Part 5 Use the Mockito method verify (confirm) that the method setLoggedIn(true) was called exactly once.

Things Created for Compilation

To get this test to compile (but not yet pass), I had to create a few interfaces and add some methods to them. I also had to create a LoginService class:

IAccount

package com.om.example.loginservice;

public interface IAccount {
   void setLoggedIn(boolean value);
   boolean passwordMatches(String candidate);
}

IAccountRepository

package com.om.example.loginservice;

public interface IAccountRepository {

	IAccount find(String accountId);
}

LoginService

package com.om.example.loginservice;

public class LoginService {
  public LoginService(IAccountRepository accountRepository) {
  }

  public void login(String accountId, String password) {
  }

}

Creating the test and adding all of theses classes gets my first test to Red with the following error:

org.mockito.exceptions.verification.WantedButNotInvoked: 
Wanted but not invoked:
iAccount.setLoggedIn(true);
	at com.om.example.loginservice.LoginServiceTest.ItShouldSetAccountToLoggedInWhen
PasswordMatches(LoginServiceTest.java:16)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	<<snip>>

While the stack trace looks a little daunting, the error seems clear enough. As you’ll see, adding a little bit of code in the LoginService class will get the test passing.

Code Updated to get Test to turn Green

Update: LoginService

The test as written requires that the production code (LoginService) sends a message to a particular IAccount object. The LoginService retrieves accounts via its IAccountRepository, which it received during construction. So all we need to do is remember that particular IAccountRepository object and use it:

package com.om.example.loginservice;

public class LoginService {
   private final IAccountRepository accountRepository;

   public LoginService(IAccountRepository accountRepository) {
      this.accountRepository = accountRepository;
   }

   public void login(String accountId, String password) {
      IAccount account = accountRepository.find(accountId);
      account.setLoggedIn(true);
   }

}

Test 2: 3 Failed Logins Causes Account to be Revoked

After three consecutive failed login attempts to the account, the account shall be revoked. Here’s such a test expressing this business rule (we’ll remove duplication in the tests after getting to green):

   @Test
   public void itShouldSetAccountToRevokedAfterThreeFailedLoginAttempts() {
      IAccount account = mock(IAccount.class);
      when(account.passwordMatches(anyString())).thenReturn(false);

      IAccountRepository accountRepository = mock(IAccountRepository.class);
      when(accountRepository.find(anyString())).thenReturn(account);

      LoginService service = new LoginService(accountRepository);

      for (int i = 0; i < 3; ++i)
         service.login("brett", "password");

      verify(account, times(1)).setRevoked(true);
   }

Test Description

As before, there are 5 parts to this test:

Part 1 Create an IAccount test-double. Unlike the first test, this test double never matches any password.
Part 2 Create an IAccountRepository test-double and register the IAccount test-double with it for any account id.
Part 3 Create the LoginService as before, injecting the IAccountRepository test-double.
Part 4 Attempt to login three times, each time should fail.
Part 5 Finally, verify that the account was set to revoked after three times.

Notice that this test does not check that setLogedIn is not called. It certainly could and that would make it in a sense more complete. On the other hand, it would also tie the test verification to the underlying implementation and also be testing something that might better be created as its own test (so that’s officially on the punch-list for later implementation).

Things Created for Compilation

This test requires a new method, setRevoked(boolean value) to be added to the IAccount interface.

When you’ve done that, the test fails with an exception similar to the previous test. Next, it’s time to make the test turn green.

Code Updated to get Test to turn Green

Here’s one way to make this test pass (and keep the first test passing):

   private int failedAttempts = 0;
      // snip ...

   public void login(String accountId, String password) {
      IAccount account = accountRepository.find(accountId);
      account.setLoggedIn(true);
      if (!account.passwordMatches(password))
         ++failedAttempts;
      if (failedAttempts == 3)
         account.setRevoked(true);
   }

Sure it is a bit ugly and we can certainly improve on the structure. Before doing that, however, we’ll let the production code ripen a bit to get a better sense of its direction. Instead, let’s spend some time removing duplication in the unit test code. Rather than make you work through several refactoring steps, here’s the final version I came up with:

package com.om.example.loginservice;

import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import org.junit.Before;
import org.junit.Test;

public class LoginServiceTest {
   private IAccount account;
   private IAccountRepository accountRepository;
   private LoginService service;

   @Before
   public void init() {
      account = mock(IAccount.class);
      accountRepository = mock(IAccountRepository.class);
      when(accountRepository.find(anyString())).thenReturn(account);
      service = new LoginService(accountRepository);
   }

   private void willPasswordMatch(boolean value) {
      when(account.passwordMatches(anyString())).thenReturn(value);
   }

   @Test
   public void itShouldSetAccountToLoggedInWhenPasswordMatches() {
      willPasswordMatch(true);
      service.login("brett", "password");
      verify(account, times(1)).setLoggedIn(true);
   }

   @Test
   public void itShouldSetAccountToRevokedAfterThreeFailedLoginAttempts() {
      willPasswordMatch(false);

      for (int i = 0; i < 3; ++i)
         service.login("brett", "password");

      verify(account, times(1)).setRevoked(true);
   }
}

This simply extracts common setup to an init() method. However, this cleanup really shortens the individual tests considerably. It also makes their intent clearer.

Test 3: setLoggedIn not called if password does not match

The first two tests have made good progress, however to keep the number of assertions per test small (so far one) and to make individual tests less dependent on the underlying implementation, this next test forces a fix to the code and probably would have been a better second test than one you just created.

import static org.mockito.Mockito.never;
...
   @Test
   public void itShouldNotSetAccountLoggedInIfPasswordDoesNotMatch() {
      willPasswordMatch(false);
      service.login("brett", "password");
      verify(account, never()).setLoggedIn(true);
   }

Test Description

This test takes advantage of the recent test refactoring. Before ever getting into the test method, the init() method:

There’s not much left:

It would have been reasonable to use a strict mock - one that does not allow any method invocations not explicitly specified. However, in this example I’m shying away from strict mocks.

Things Created for Compilation

This test did not require any existing classes to have new methods added.

Once the test executes, you’ll notice a failure. It’s a bit different from the previous example, but still it is fairly clear what happened. A method that should not have been called was called:

org.mockito.exceptions.verification.NeverWantedButInvoked: 
iAccount.setLoggedIn(true);
Never wanted but invoked!
	at com.om.example.loginservice.LoginServiceTest.ItShouldNotSetAccountLoggedInIf
PasswordDoesNotMatch(LoginServiceTest.java:51)
Caused by: org.mockito.exceptions.cause.UndesiredInvocation: 
Undesired invocation:
	at com.om.example.loginservice.LoginService.login(LoginService.java:15)
	at com.om.example.loginservice.LoginServiceTest.ItShouldNotSetAccountLoggedInIf
PasswordDoesNotMatch(LoginServiceTest.java:50)
	<<snip>>

Code Updated to get Test to turn Green

The LoginService.login method needs a little updating:

   public void login(String accountId, String password) {
      IAccount account = accountRepository.find(accountId);

      if (account.passwordMatches(password))
         account.setLoggedIn(true);
      else
         ++failedAttempts;

      if (failedAttempts == 3)
         account.setRevoked(true);
   }

Verify that your code compiles and your tests pass.

Test 4: Two Fails on One Account Followed By Fail on Second Account

This is one of those requirements you ask “Really?!” This requirement comes from an actual project, so while it might sound bogus, it is an actual requirement from the real world.

   @Test
   public void itShouldNotRevokeSecondAccountAfterTwoFailedAttemptsFirstAccount() {
      willPasswordMatch(false);

      IAccount secondAccount = mock(IAccount.class);
      when(secondAccount.passwordMatches(anyString())).thenReturn(false);
      when(accountRepository.find("schuchert")).thenReturn(secondAccount);

      service.login("brett", "password");
      service.login("brett", "password");
      service.login("schuchert", "password");

      verify(secondAccount, never()).setRevoked(true);
   }

Test Description

This test is a little longer because it requires more setup. Rather than possibly messing up existing tests and adding more setup to the fixture, I decided to do it in this test. There are alternatives to writing this test’s setup:

Since my primary purpose of this tutorial is practice using Mockito, I’ll leave it as is until I notice additional duplication.

There are 4 parts to this test:

Part 1 Set the password matching to false on the account.
Part 2 Create a second account, with a never-matching password and register it with the account repository. Notice that this uses a particular account name, “schuchert”. Mockito, notices more specificwhen clauses over more general ones, so adding this after saying “for any string” is OK. This is a convenient default behavior (or is that behaviour as they would spell it?-).
Part 3 Login two times to the first account (both failing), then log in to a second account, also failing. That’s three failures in a row, but to two different accounts, so no account should be revoked.
Part 4 Verify that the secondAccount is not revoked.

Things Created for Compilation

This test compiles without any new methods. It does fail with the following exception:

org.mockito.exceptions.verification.NeverWantedButInvoked: 
iAccount.setRevoked(true);
Never wanted but invoked!
	at com.om.example.loginservice.LoginServiceTest.ItShouldNotRevokeSecondAccount
AfterTwoFailedAttemptsFirstAccount(LoginServiceTest.java:66)
Caused by: org.mockito.exceptions.cause.UndesiredInvocation: 
Undesired invocation:
	at com.om.example.loginservice.LoginService.login(LoginService.java:21)
	<snip>

As with previous exceptions, the message tells you what you need to know. The account was incorrectly revoked.

Code Updated to get Test to turn Green

To get this new test to pass, I added a new attribute to the LoginService class: previousAccountId. Then I updated the login method to take advantage of it:

   private String previousAccountId = "";
      // snip ...

   public void login(String accountId, String password) {
      IAccount account = accountRepository.find(accountId);

      if (account.passwordMatches(password)) {
         account.setLoggedIn(true);
      } else {
         if (previousAccountId.equals(accountId))
            ++failedAttempts;
         else {
            failedAttempts = 1;
            previousAccountId = accountId;
         }
      }

      if (failedAttempts == 3)
         account.setRevoked(true);
   }

This allows all tests to pass. Would it have been possible to do less? Maybe, but this was the first thing that came to mind. The code is starting to be a bit unruly. We’re just about ready to clean up this code, but before we do there are a few more tests.

Test 5: Do not allow a second login

In the actual problem, counting concurrent logins was somewhat complex. For this example, we’ll keep it simple. If you are already logged in, you cannot log in a second time. That’s simple enough:

   @Test(expected = AccountLoginLimitReachedException.class)
   public void itShouldNowAllowConcurrentLogins() {
      willPasswordMatch(true);
      when(account.isLoggedIn()).thenReturn(true);
      service.login("brett", "password");
   }

Test Description

This test first sets the password to matching. However, it also sets a new method, isLoggedIn, to always return true. It then attempts to login. The validation part of this test is in the (expected = AccountLoginLimitReachedException.class) part of the annotation.

Things Created for Compilation

First, create the new exception:

AccountLoginLimitReachedException

package com.om.example.loginservice;

public class AccountLoginLimitReachedException extends RuntimeException {
   private static final long serialVersionUID = 1L;
}

Next, add a new method to the IAccount class, isLoggedIn.

When you make these changes, the test will fail and the message indicates it expected an exception.

Code Updated to get Test to turn Green

To get that exception thrown, simply make one small addition to the login method:

   public void login(String accountId, String password) {
      IAccount account = accountRepository.find(accountId);

      if (account.passwordMatches(password)) {
         if (account.isLoggedIn())
            throw new AccountLoginLimitReachedException();
         account.setLoggedIn(true);
      } else {
      // snip ...

Test 6: AccountNotFoundException thrown if account is not found

This is a final test to make sure the code handles the case of an account not getting found. This is not too hard to write:

   @Test(expected = AccountNotFoundException.class)
   public void ItShouldThrowExceptionIfAccountNotFound() {
      when(accountRepository.find("schuchert")).thenReturn(null);
      service.login("schuchert", "password");
   }

Test Description

This test takes advantage of the fact that more specificwhen clauses take precedence over more general ones. This test configures the account repository test-double to return null for the account “schuchert”. It then attempts the login, which should throw an exception.

Things Created for Compilation

To get this test to compile, you’ll need to add a new exception class:

AccountNotFoundException

package com.om.example.loginservice;

public class AccountNotFoundException extends RuntimeException {
   private static final long serialVersionUID = 1L;
}

Code Updated to get Test to turn Green

When you make this change, the test will fail with a null pointer exception. The fix is quick and at the top of the method:

   public void login(String accountId, String password) {
      IAccount account = accountRepository.find(accountId);

      if (account == null)
         throw new AccountNotFoundException();
      // snip ...

This should make all tests pass.

Test 7: Cannot Login to Revoked Account

The next test is similar to the previous test. A revoked account does not allow logins:

   @Test(expected = AccountRevokedException.class)
   public void ItShouldNotBePossibleToLogIntoRevokedAccount() {
      willPasswordMatch(true);
      when(account.isRevoked()).thenReturn(true);
      service.login("brett", "password");
   }

Test Description

This test is a repeat of the previous test, checking for a different result from a different starting condition.

Things Created for Compilation

You’ll need to add another exception, AccountRevokedException (as an unchecked exception) and a new method, isRevoked, to IAccount.

Code Updated to get Test to turn Green

The only update to get to green is adding a check - a guard clause - similar to the previous test:

   public void login(String accountId, String password) {
      IAccount account = accountRepository.find(accountId);

      if (account == null)
         throw new AccountNotFoundException();

      if (account.passwordMatches(password)) {
         if (account.isLoggedIn())
            throw new AccountLoginLimitReachedException();
         if (account.isRevoked())
            throw new AccountRevokedException();
      // snip ...

Summary so far

There are many more tests you could add to this system:

So there’s a lot let to make this a complete system. Even so, the code in the LoginService.login method is unruly. There are two problems:

The first issue suggests spending some time on an Account class and then moving some of the responsibility from the LoginService class to that new Account class. For example, instead of setLoggedIn(true), change it to login() and then respond accordingly:

If you do this, then you’ll be able to simplify the LoginServiceTest class because some of the tests will no longer belong there and instead will exist on the AccountTest. Creating AccountTest and Account classes is left as an exercise to the reader.

The second issue suggests the GoF State pattern. And in fact, that’s the next section.

Refactoring LoginService

In the real system, there were more requirements and the stream of requirements were fed to me over months. The underlying login service I created looked something like this simple version, just bigger. On the real project, the code became very hard to manage because I was not practicing refacoring aggressively enough at the time. I realized that the underlying solution would be made better by applying the GoF State pattern. In the actual solution, the LoginService had several methods, with many of the methods’ responses dependent on either the state of the login service or the account.

I made the change and sure enough supporting new requirements wasmuch easier. The remainder of this tutorial involves refactoring the current solution to use the GoF State pattern.

Here’s where we’re going:

How Does the State Pattern Apply?

In the typical state pattern, all or part of an object’s behavior depends on what has happened to it in the past. In this case there are two different sets of state:

Given the requirements so far, the state pattern is overkill. However, in the real system, refactoring to the state pattern made the implementationmuch easier, and more reliable as it turns out. So we’ll migrate the current solution of the LoginState.login method to delegate some of the responsibility to a state object.

Refactoring to the State Pattern

We have tests passing and green. Our goal is to slowly migrate the code. Along the way we’ll also clean up some other problems.

Here’s our starting point:

   public void login(String accountId, String password) {
      IAccount account = accountRepository.find(accountId);

      if (account == null)
         throw new AccountNotFoundException();

      if (account.passwordMatches(password)) {
         if (account.isLoggedIn())
            throw new AccountLoginLimitReachedException();
         if (account.isRevoked())
            throw new AccountRevokedException();
         account.setLoggedIn(true);
      } else {
         if (previousAccountId.equals(accountId))
            ++failedAttempts;
         else {
            failedAttempts = 1;
            previousAccountId = accountId;
         }
      }

      if (failedAttempts == 3)
         account.setRevoked(true);
   }

This refactoring is a simplified version of Replace Type Code with State/Strategy. What we’ll do is somewhat simpler because we do not have a type code. Rather, the underlying code is state-based and it is this observation, along with difficulty of managing the code, that suggests following the refactoring steps described in Martin Fowler’s Refactoring book to get to the state pattern. And that is what follows.

Create Hierarchy

Remember that refactoring is an attempt to improve the structure of the code without affecting the code’s behavior. In our case, the behavior is defined by our existing unit tests. We’ll take many small steps that have two goals in mind:

With this in mind our first effort will be to create the basic scaffolding. To do that:

Notice that you are not first creating unit tests. We already have unit tests in place. We are restructuring the code and the existing unit tests should keep us honest.

Also notice that for this first step, you’ve done nothing that will cause code to not compile or tests to fail. Go ahead and make sure your tests still pass, but that should be done by reflex anyway.

Copy Behavior from Source To Abstract Base Class

To keep things compiling and tests running, you’ll often do the following general steps:

That is, refactoring is more about copy, update, remove rather than directly moving code. This may take a bit longer, but it is less risky, keeps code compiling more often and makes it less likely you’ll break tests and lost track of what’s next.

In the case of moving to the state pattern, typically the method on the state object takes in a so-called context object. In this example, the context object is the LoginService object. Also, before we make the move, consider the parameters: a string for the account id and a string for the password. If you pass the account string into the LoginServiceState, then the state will have to use the account repository to look up the service. That’s fine, but it requires injection of the account repository into each of the state instances. Rather than doing that, we will instead pass in an IAccount. We’ll allow the lookup to happen in the LoginService and the resulting IAccount will be passed into the state.

Here is a first cut at creating that new method in LoginServiceState:

   public void login(LoginService context, IAccount account, String password) {
      if (account.passwordMatches(password)) {
         if (account.isLoggedIn())
            throw new AccountLoginLimitReachedException();
         if (account.isRevoked())
            throw new AccountRevokedException();
         account.setLoggedIn(true);
      } else {
         if (previousAccountId.equals(accountId))
            ++failedAttempts;
         else {
            failedAttempts = 1;
            previousAccountId = accountId;
         }
      }

      if (failedAttempts == 3)
         account.setRevoked(true);
   }

If you create this method, you’ll notice the following problems:

So as it is, we cannot add this method to the LoginServiceState class until we make a few changes. Rather than add the method and make the changes, back out this change and instead fix the problem of the accountId not being available first.

In fact, there’s a set of refactorings we can do that will support this change:

Add getId to IAccount

Simply add this method to IAccount: String getId();

Update LoginService.login to use getId()

There are only two lines in the bottom of the login method that use accountId (other than the first time it is used to look up the account). Those need to change:

      } else {
         if (previousAccountId.equals(account.getId()))
            ++failedAttempts;
         else {
            failedAttempts = 1;
            previousAccountId = account.getId();
         }
      }

Run your tests and you’ll see that two fail.

Get tests to pass again

A single line added to the init() method in the unit test will get this to pass:

   public void init() {
      account = mock(IAccount.class);
      when(account.getId()).thenReturn("brett");
      // snip ...

Verify your tests all pass.

Extract the body of the method

Finally, extract the bottom part of the method:

   public void login(String accountId, String password) {
      IAccount account = accountRepository.find(accountId);

      if (account == null)
         throw new AccountNotFoundException();

      verifyLoginAttempt(account, password);
   }

   private void verifyLoginAttempt(IAccount account, String password) {
      if (account.passwordMatches(password)) {
         if (account.isLoggedIn())
            throw new AccountLoginLimitReachedException();
         if (account.isRevoked())
            throw new AccountRevokedException();
         account.setLoggedIn(true);
      } else {
         if (previousAccountId.equals(account.getId()))
            ++failedAttempts;
         else {
            failedAttempts = 1;
            previousAccountId = account.getId();
         }
      }

      if (failedAttempts == 3)
         account.setRevoked(true);
   }

Verify your tests all pass.

Copy (with rename) verifyLoginAttempt into LoginServiceState.login

Notice that the code is well prepared to handle logging in after the IAccount is found. Start by copying verifyLoginAttempt into LoginServceState, rename it to login and add the missing previousAccountId and failedAttempts fields into the state object:

package com.om.example.loginservice;

public abstract class LoginServiceState {
   private String previousAccountId = "";
   private int failedAttempts;

   public void login(IAccount account, String password) {
      if (account.passwordMatches(password)) {
         if (account.isLoggedIn())
            throw new AccountLoginLimitReachedException();
         if (account.isRevoked())
            throw new AccountRevokedException();
         account.setLoggedIn(true);
      } else {
         if (previousAccountId.equals(account.getId()))
            ++failedAttempts;
         else {
            failedAttempts = 1;
            previousAccountId = account.getId();
         }
      }

      if (failedAttempts == 3)
         account.setRevoked(true);
   }
}

Verify your code compiles and your tests pass.

Deleage to State

You’ll take three steps to complete this:

Add State Instance to LoginServce

Next, modify the LoginService to have an instance of LoginServiceState and initialize it to AwaitingFirstLoginAttempt:

   private LoginServiceState state = new AwaitingFirstLoginAttempt();

Your code should still compile and your tests should still pass.

Delegate to State instace

Update the login method to delegate to the state:

   public void login(String accountId, String password) {
      IAccount account = accountRepository.find(accountId);

      if (account == null)
         throw new AccountNotFoundException();

      state.login(account, password);
   }

Your code should still compile and your tests should still pass.

Remove Stale Code

Finally, remove the verifyLoginAttempt method and the failedAttempts and previousAccountId fields:

package com.om.example.loginservice;

public class LoginService {
   private final IAccountRepository accountRepository;
   private LoginServiceState state = new AwaitingFirstLoginAttempt();

   public LoginService(IAccountRepository accountRepository) {
      this.accountRepository = accountRepository;
   }

   public void login(String accountId, String password) {
      IAccount account = accountRepository.find(accountId);

      if (account == null)
         throw new AccountNotFoundException();

      state.login(account, password);
   }
}

Prepare for Polymorphsim

Next, you’ll push the LoginServiceState.login method to each of the subclasses and then begin to remove the code not specifically related to each of the states.

Copy Code

First, push the login method as is into each of the subclasses and make the method abstract in the base class. (Note in most Java IDE’s this is a single refactoring command.)

Here’s the resulting LoginServiceState:

package com.om.example.loginservice;

public abstract class LoginServiceState {
   protected String previousAccountId = "";
   protected int failedAttempts;

   public abstract void login(IAccount account, String password);
}

And here’s one of the substates:

package com.om.example.loginservice;

public class AwaitingFirstLoginAttempt extends LoginServiceState {
   @Override
   public void login(IAccount account, String password) {
      if (account.passwordMatches(password)) {
         if (account.isLoggedIn())
            throw new AccountLoginLimitReachedException();
         if (account.isRevoked())
            throw new AccountRevokedException();
         account.setLoggedIn(true);
      } else {
         if (previousAccountId.equals(account.getId()))
            ++failedAttempts;
         else {
            failedAttempts = 1;
            previousAccountId = account.getId();
         }
      }
   
      if (failedAttempts == 3)
         account.setRevoked(true);
   }
}

Verify your code compiles and your tests pass.

Remove The Part That Does Not Apply

Now for each of the sub states we’ll:

Enabling Refactoring

Note, before we can do any of what is to come, we need a way to set the next state. There are two obvious options:

Either option will work, I prefer the second option for two reasons:

In general, when I use the state pattern, I pick the second option from my previous experience with the state pattern.

If you are using a modern IDE, this is a simple refactoring. If not:

Verify that your tests still pass.

Update AwaitingFirstLoginAttempt

First, remove the parts of the login method that do not apply to the first time a password does not match:

   @Override
   public void login(LoginService context, IAccount account, String password) {
      if (account.passwordMatches(password)) {
         if (account.isLoggedIn())
            throw new AccountLoginLimitReachedException();
         if (account.isRevoked())
            throw new AccountRevokedException();
         account.setLoggedIn(true);
      } else {
         context.setState(new AfterFirstFailedLoginAttempt(account.getId()));
      }
   }

To get this to pass, you’ll need to make a few additions:

Add constructor to AfterFirstFailedLoginAttempt

   private String previousAccountId;

   public AfterFirstFailedLoginAttempt(String previousAccountId) {
      this.previousAccountId = previousAccountId;
      failedAttempts = 1;
   }

(Note: I’m cheating a bit here, I’m adding the previousAccountId as a field in this class. It will eventually be removed from the abstract base class as it does not apply to the AwaitingFirstLoginAttempt class. This is an example of avoiding violating the Liskov Substitution Principle.)

Add setState to LoginService

   public void setState(LoginServiceState state) {
      this.state = state;
   }

Verify your code compiles and the tests all pass.

Update AfterFirstFailedLoginAttempt

Moving through the state model, we’ll fix the second state:

   @Override
   public void login(LoginService context, IAccount account, String password) {
      if (account.passwordMatches(password)) {
         if (account.isLoggedIn())
            throw new AccountLoginLimitReachedException();
         if (account.isRevoked())
            throw new AccountRevokedException();
         account.setLoggedIn(true);
      } else {
         if (previousAccountId.equals(account.getId()))
            context
                  .setState(new AfterSecondFailedLoginAttempt(account.getId()));
         else
            previousAccountId = account.getId();
      }
   }

To get this to compile, you’ll need to add a constructor to AfterSecondFailedLoginAttempt:

   private String previousAccountId;

   public AfterSecondFailedLoginAttempt(String previousAccountId) {
      this.previousAccountId = previousAccountId;
      failedAttempts = 2;
   }

Verify your code compiles and your tests pass.

Update AfterSecondFailedAttempt

Now it’s time to update the final state:

   @Override
   public void login(LoginService context, IAccount account, String password) {
      if (account.passwordMatches(password)) {
         if (account.isLoggedIn())
            throw new AccountLoginLimitReachedException();
         if (account.isRevoked())
            throw new AccountRevokedException();
         account.setLoggedIn(true);
      } else {
         if (previousAccountId.equals(account.getId())) {
            account.setRevoked(true);
            context.setState(new AwaitingFirstLoginAttempt());
         } else {
            context.setState(new AfterFirstFailedLoginAttempt(account.getId()));
         }
      }
   }

Make sure your code compiles and your tests pass.

Cleanup

Here are some remaining cleanup steps (after each change, make sure your tests still pass):

Notice that there’s a lot of duplication in each of the three derived classes. Now, you’ll introduce the Gof Template Method Pattern.

Introduce the Gof Template Method Pattern

The template method pattern expresses an algorithm in a base class with extension points implemented in a derived class. The extension points are:

Some external client issues a command, say X() as in the diagram above. The method X() has a number of steps (three in this example). The first and third steps are implemented in the base class. There is one part of the algorithm, the second step, that varies. Rather than attempt to implement it, the base class defers to an abstract method. The derived classes implement that abstract method to complete the algorithm.

Consider the game Monopoly. There are three kinds of locations around the board which players may purchase. These three kinds of locations are:

There is a standard algorithm for what happens when a player lands on a location:

Landing is a standard set of steps except for rent calculation. In terms of the template method pattern, there could be an abstract base class, say Real Estate, that has a method, landOn. Most of the work of landOn is written in the Real Estate base class. However, if rent needs to be charged, the RealEstate’s landOn method can defer the details of rent calculation to an abstract method it defines.

How It Applies To LoginServiceState

In the following drawing (which attempts to follow the UML 2.0 specification), thec methodd in thet base classe is the extension point. The base class deals with the basic validation like matching passwords and revoked accounts. It only defers what happens if the password does not match to the derived classes: « need to recreat this diagram » Rather than walk you through this refactoring, I’m just going to give you each of the classes.

Update: LoginServiceState

package com.om.example.loginservice;

public abstract class LoginServiceState {
   public final void login(LoginService context, IAccount account,
         String password) {
      if (account.passwordMatches(password)) {
         if (account.isLoggedIn())
            throw new AccountLoginLimitReachedException();
         if (account.isRevoked())
            throw new AccountRevokedException();
         account.setLoggedIn(true);
      } else
         handleIncorrectPassword(context, account, password);
   }
   
   public abstract void handleIncorrectPassword(LoginService context,
         IAccount account, String password);
}

Update: AwaitingFirstLoginAttempt

package com.om.example.loginservice;

public class AwaitingFirstLoginAttempt extends LoginServiceState {
   @Override
   public void handleIncorrectPassword(LoginService context, IAccount account,
         String password) {
      context.setState(new AfterFirstFailedLoginAttempt(account.getId()));
   }
}

Update: AfterFirstFailedLoginAttempt

package com.om.example.loginservice;

public class AfterFirstFailedLoginAttempt extends LoginServiceState {
   private String previousAccountId;

   public AfterFirstFailedLoginAttempt(String previousAccountId) {
      this.previousAccountId = previousAccountId;
   }

   @Override
   public void handleIncorrectPassword(LoginService context, IAccount account,
         String password) {
      if (previousAccountId.equals(account.getId()))
         context.setState(new AfterSecondFailedLoginAttempt(account.getId()));
      else
         previousAccountId = account.getId();
   }
}

Update: AfterSecondFailedLoginAttempt

package com.om.example.loginservice;

public class AfterSecondFailedLoginAttempt extends LoginServiceState {
   private String previousAccountId;

   public AfterSecondFailedLoginAttempt(String previousAccountId) {
      this.previousAccountId = previousAccountId;
   }

   @Override
   public void handleIncorrectPassword(LoginService context, IAccount account,
         String password) {
      if (previousAccountId.equals(account.getId())) {
         account.setRevoked(true);
         context.setState(new AwaitingFirstLoginAttempt());
      } else {
         context.setState(new AfterFirstFailedLoginAttempt(account.getId()));
      }
   }
}

Verify It All Works

After these four changes, make sure your code compiles and the tests pass.

Final Cleanup, Part Two

When I looked at the login method in the LoginServcieState I realized there was a missing test. I also did not like the violation of the Dependency Inversion Principle. So we’ll fix those two things.

Add a Missing Test

There is a problem with the current implementation of LoginServiceState.login, but there are no tests to verify that the problem exists. Rather than tell you what the problem is, here is one final test:

   @Test
   public void itShouldResetBackToInitialStateAfterSuccessfulLogin() {
      willPasswordMatch(false);
      service.login("brett", "password");
      service.login("brett", "password");
      willPasswordMatch(true);
      service.login("brett", "password");
      willPasswordMatch(false);
      service.login("brett", "password");
      verify(account, never()).setRevoked(true);
   }

And here’s a fix to make this pass. In the LoginServiceState.login method, add the following line:

         context.setState(new AwaitingFirstLoginAttempt());

After this line:

         account.setLoggedIn(true);

What the first test does and why this fix works is left to the reader as an exercise.

Remove Dependency Inversion Principle Violation

The abstract class LoginServiceState depends on the concrete LoginService class, which violates the Dependency Inversion Principle. This is probably OK given that the state pattern is really a way to take part of the implementation of a class and extract it to a hierarchy. The combination of LoginService plus the LoginServiceState hierarchy is really a single logical unit.

Even so, let’s take this to its logical (extreme) conclusion as a way to demonstrate taking something too far.

Extract Class

Extract a base class for LoginService:

LoginServiceContext

package com.om.example.loginservice;

public abstract class LoginServiceContext {

   private LoginServiceState state;

   public LoginServiceContext(LoginServiceState state) {
      this.state = state;
   }

   public void setState(LoginServiceState state) {
      this.state = state;
   }

   public LoginServiceState getState() {
      return state;
   }
}

Update: LoginService

package com.om.example.loginservice;

public class LoginService extends LoginServiceContext {
   private final IAccountRepository accountRepository;

   public LoginService(IAccountRepository accountRepository) {
      super(new AwaitingFirstLoginAttempt());
      this.accountRepository = accountRepository;
   }

   public void login(String accountId, String password) {
      IAccount account = accountRepository.find(accountId);

      if (account == null)
         throw new AccountNotFoundException();

      getState().login(this, account, password);
   }
}

Replace all uses of LoginService with LoginServiceContext in the LoginServiceState hierarchy. Note that when I used the extract superclass refactoring in Eclipse, this was done automatically.

Make sure your code compiles and your tests pass.

Summary

Congratulations. You started by writing tests using Mockito. Once you had a number of tests in place, you refactored your production code from a bunch of nested if statements (an embedded state machine) to use the GoF State pattern. Once you got that working and cleaned up, you removed further duplication by introducing the Gof Template Method Pattern.

If you want to update your resume, it’s time to add:

I hope you enjoyed your journey.

The Final Source Code

LoginServiceTest.java

package com.om.example.loginservice;

import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import org.junit.Before;
import org.junit.Test;

public class LoginServiceTest {
   private IAccount account;
   private IAccountRepository accountRepository;
   private LoginService service;

   @Before
   public void init() {
      account = mock(IAccount.class);
      when(account.getId()).thenReturn("brett");
      accountRepository = mock(IAccountRepository.class);
      when(accountRepository.find(anyString())).thenReturn(account);
      service = new LoginService(accountRepository);
   }

   private void willPasswordMatch(boolean value) {
      when(account.passwordMatches(anyString())).thenReturn(value);
   }

   @Test
   public void itShouldSetAccountToLoggedInWhenPasswordMatches() {
      willPasswordMatch(true);
      service.login("brett", "password");
      verify(account, times(1)).setLoggedIn(true);
   }

   @Test
   public void itShouldSetAccountToRevokedAfterThreeFailedLoginAttempts() {
      willPasswordMatch(false);

      for (int i = 0; i < 3; ++i)
         service.login("brett", "password");

      verify(account, times(1)).setRevoked(true);
   }

   @Test
   public void itShouldNotSetAccountLoggedInIfPasswordDoesNotMatch() {
      willPasswordMatch(false);
      service.login("brett", "password");
      verify(account, never()).setLoggedIn(true);
   }

   @Test
   public void itShouldNotRevokeSecondAccountAfterTwoFailedAttemptsFirstAccount() {
      willPasswordMatch(false);

      IAccount secondAccount = mock(IAccount.class);
      when(secondAccount.passwordMatches(anyString())).thenReturn(false);
      when(accountRepository.find("schuchert")).thenReturn(secondAccount);

      service.login("brett", "password");
      service.login("brett", "password");
      service.login("schuchert", "password");

      verify(secondAccount, never()).setRevoked(true);
   }

   @Test(expected = AccountLoginLimitReachedException.class)
   public void itShouldNowAllowConcurrentLogins() {
      willPasswordMatch(true);
      when(account.isLoggedIn()).thenReturn(true);
      service.login("brett", "password");
   }

   @Test(expected = AccountNotFoundException.class)
   public void itShouldThrowExceptionIfAccountNotFound() {
      when(accountRepository.find("schuchert")).thenReturn(null);
      service.login("schuchert", "password");
   }

   @Test(expected = AccountRevokedException.class)
   public void ItShouldNotBePossibleToLogIntoRevokedAccount() {
      willPasswordMatch(true);
      when(account.isRevoked()).thenReturn(true);
      service.login("brett", "password");
   }

   @Test
   public void itShouldResetBackToInitialStateAfterSuccessfulLogin() {
      willPasswordMatch(false);
      service.login("brett", "password");
      service.login("brett", "password");
      willPasswordMatch(true);
      service.login("brett", "password");
      willPasswordMatch(false);
      service.login("brett", "password");
      verify(account, never()).setRevoked(true);
   }
}

LoginService.java

package com.om.example.loginservice;

public class LoginService extends LoginServiceContext {
   private final IAccountRepository accountRepository;

   public LoginService(IAccountRepository accountRepository) {
      super(new AwaitingFirstLoginAttempt());
      this.accountRepository = accountRepository;
   }

   public void login(String accountId, String password) {
      IAccount account = accountRepository.find(accountId);

      if (account == null)
         throw new AccountNotFoundException();

      getState().login(this, account, password);
   }
}

LoginServiceContext.java

package com.om.example.loginservice;

public abstract class LoginServiceContext {
   private LoginServiceState state;

   public LoginServiceContext(LoginServiceState state) {
      this.state = state;
   }

   public void setState(LoginServiceState state) {
      this.state = state;
   }

   public LoginServiceState getState() {
      return state;
   }
}

LoginServiceState.java

package com.om.example.loginservice;

public abstract class LoginServiceState {
   public final void login(LoginServiceContext context, IAccount account,
         String password) {
      if (account.passwordMatches(password)) {
         if (account.isLoggedIn())
            throw new AccountLoginLimitReachedException();
         if (account.isRevoked())
            throw new AccountRevokedException();
         account.setLoggedIn(true);
         context.setState(new AwaitingFirstLoginAttempt());
      } else
         handleIncorrectPassword(context, account, password);
   }

   public abstract void handleIncorrectPassword(LoginServiceContext context,
         IAccount account, String password);
}

AwaitingFirstLoginAttempt.java

package com.om.example.loginservice;

public class AwaitingFirstLoginAttempt extends LoginServiceState {
   @Override
   public void handleIncorrectPassword(LoginServiceContext context, IAccount account,
         String password) {
      context.setState(new AfterFirstFailedLoginAttempt(account.getId()));
   }
}

AfterFirstFailedLoginAttempt.java

package com.om.example.loginservice;

public class AfterFirstFailedLoginAttempt extends LoginServiceState {
   private String previousAccountId;

   public AfterFirstFailedLoginAttempt(String previousAccountId) {
      this.previousAccountId = previousAccountId;
   }

   @Override
   public void handleIncorrectPassword(LoginServiceContext context, IAccount account,
         String password) {
      if (previousAccountId.equals(account.getId()))
         context.setState(new AfterSecondFailedLoginAttempt(account.getId()));
      else
         previousAccountId = account.getId();
   }
}

AfterSecondFailedLoginAttempt.java

package com.om.example.loginservice;

public class AfterSecondFailedLoginAttempt extends LoginServiceState {
   private String previousAccountId;

   public AfterSecondFailedLoginAttempt(String previousAccountId) {
      this.previousAccountId = previousAccountId;
   }

   @Override
   public void handleIncorrectPassword(LoginServiceContext context, IAccount account,
         String password) {
      if (previousAccountId.equals(account.getId())) {
         account.setRevoked(true);
         context.setState(new AwaitingFirstLoginAttempt());
      } else {
         context.setState(new AfterFirstFailedLoginAttempt(account.getId()));
      }
   }
}

IAccount.java

package com.om.example.loginservice;

public interface IAccount {
   boolean passwordMatches(String candiate);
   void setLoggedIn(boolean value);
   void setRevoked(boolean value);
   boolean isLoggedIn();
   boolean isRevoked();
   String getId();
}

IAccountRepository.java

package com.om.example.loginservice;

public interface IAccountRepository {
  IAccount find(String accountId);
}

AccountLoginLimitReachedException.java

package com.om.example.loginservice;

public class AccountLoginLimitReachedException extends RuntimeException {
   private static final long serialVersionUID = 1L;
}

AccountNotFoundException.java

package com.om.example.loginservice;

public class AccountNotFoundException extends RuntimeException {
   private static final long serialVersionUID = 1L;
}

AccountRevokedException.java

package com.om.example.loginservice;

public class AccountRevokedException extends RuntimeException {
   private static final long serialVersionUID = 1L;
}
" Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.