Tuesday, May 25, 2010

FakeItEasy Login Service Example Series – Part 4

This is the fourth part in the series of posts where I’m porting Brett Schucherts excelent demo of Mockito in Java to C# and FakeItEasy.

The source for this example series can be found in a Mercurial repository at Google code. Each test implementation and following code update is a separate commit so you can easily update your repository to look at the full code at any given state. Find the repository here.

Part 1 can be found here.

Part 2 can be found here.

Part 3 can be found here.

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.

The Test

[Test]
public void Should_not_revoke_other_account_when_first_account_has_failed_to_log_in_twice()
{
    // Arrange
    A.CallTo(() => this.account.PasswordMatches(A<string>.Ignored)).Returns(false);

    var secondAccount = A.Fake<IAccount>();
    A.CallTo(() => secondAccount.PasswordMatches(A<string>.Ignored)).Returns(false);
    A.CallTo(() => this.accountRepository.Find("other username")).Returns(secondAccount);
    
    // Act
    this.service.Login("username", "wrong password");
    this.service.Login("username", "wrong password");
    this.service.Login("other username", "wrong password");

    // Assert
    A.CallTo(() => secondAccount.SetRevoked(true)).MustNotHaveHappened();
}

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:

  • Leave it as is, it's not too bad.
  • Add the additional setup to the SetUp() method, making this a larger fixture.
  • Put this test is another fixture, different setup --> different fixture (this would be more of a BDD style).

Since my primary purpose of this tutorial is to demo FakeItEasy, I'll leave it as is until I notice additional duplication.

There are 4 parts to this test:

  1. Set the password matching to false on the account.
  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, "other username". FakeItEasy, will use the latest configured call before any earlier configurations so this rule has precedence over the configuration in the setup that matches any account name.
  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.
  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:

Assertion failed for the following call:
  'FakeItEasy.Examples.LoginService.IAccount.SetRevoked(True)'
Expected to find it exactly never but found it #1 times among the calls:
  1.  'FakeItEasy.Examples.LoginService.IAccount.PasswordMatches("wrong password")'
  2.  'FakeItEasy.Examples.LoginService.IAccount.SetRevoked(True)'

Code Updated to get Test to turn Green

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

namespace FakeItEasy.Examples.LoginService
{
    using System;

    public class LoginService
    {
        private IAccountRepository accountRepository;
        private int numberOfFailedAttempts;
        private string previousUsername;

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

        public void Login(string username, string password)
        {
            var account = this.accountRepository.Find(username);

            if (account.PasswordMatches(password))
            {
                account.SetLoggedIn(true);
            }
            else
            {
                if (this.previousUsername.Equals(username))
                {
                    this.numberOfFailedAttempts++;
                }
                else
                {
                    this.numberOfFailedAttempts = 1;
                    this.previousUsername = username;
                }
            }

            if (this.numberOfFailedAttempts == 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.

4 comments:

  1. Sorry for the off topic. But right now I'm pretty dummed down. Hopefully you can help me out. I've been porting my tests from RhinoMocks to FakeItEasy. You should know that the tests were written 3-4 years ago. So they might not be the best way to test code but...
    There was a test that verified that ,on a fake, a property setter was called with a specific value.
    I joyfully converted the rhino version and got : A.CallTo(()=>timer.Interval=TimeSpan.FromMinutes(1)).MustHaveHappened();
    This does not compile: error CS0832: An expression tree may not contain an assignment operator.
    What would be an alternative syntax for this case?

    ReplyDelete
  2. If the Interval property is read/write you can just check the value like this:

    Assert.That(timer.Interval, Is.EqualTo(TimeSpan.FromMinutes(1));

    If the Interval property is write only this is something that is not really well supported in FakeItEasy, mainly since I believe that a set only property may not be the best design choice for an interface. However you can do it quite easily like this:

    FakeItEasy.VisualBasic.NextCall.To(timer).WhenArgumentsMatch(x => x.Get(0) == TimeSpan.FromMinutes(1)).MustHaveHappened();
    timer.Interval = new TimeSpan();

    ReplyDelete
  3. Told you i was dummy. Thanks a lot. Sorry for the inconvenience.

    ReplyDelete
  4. Don't mention it man, hope you like FakeItEasy!

    ReplyDelete