Lesson #2 - Use JSON Objects

This lesson will show you how to take the code below and convert it into something that is easier to test and maintain. While there is nothing wrong with the code from a business rules point of view, it was not written with testing in mind.

function submitUserForm() {
      if ($('#name').val() !== "" && $('#password').val() !== "" && $('#emailAddress').val() !== "" && $('#zipCode').val() !== "") {

      $.ajax({
        url: 'http://jsfiddle.net/echo/json/',
        type: 'POST',
        dataType: 'json',
        data: {
          name: $('#name').val(),
          password: $('#password').val(),
          emailAddress: $('#emailAddress').val(),
          zipCode: $('#zipCode').val()
          },
          success: function() {
            $('#message').hide();
          },
            error: function (message) {
              $('#message').html(message);
              $('#message').show();
            }
          });
      }
    }

The first improvement to make to this code is to make use of JavaScript objects. If you want to boil it down, looks and acts an awfully lot like JSON.

var user = {
  name: $('#name').val(),
  password: $('#password').val(),
  emailAddress: $('#emailAddress').val(),
  zipCode: $('#zipCode').val()
};

By doing this, the code has taken the first step to separating out concerns. The code to access the form data has now been isolated away from the validation code. All the validation code now has to worry about is making sure data sent to it is valid. It does not have to worry about querying the DOM. This is really the core of what TDD is all about, isolating the code to make it easier to test.

function validateUser(user) {
  var errorMessage = '';
  errorMessage += validateRequiredField(user.name, "Name");
  errorMessage += validatePassword(user.password);
  errorMessage += validateEmailAddress(user.emailAddress);
  errorMessage += validateZipCode(user.zipCode);
  return errorMessage;
}
function validateRequiredField(field, fieldName) {
  if (!field || field.length === 0) {
    return fieldName + ' is required';
  }
  return '';
}
function validatePassword(password) {
  if (!password || password.length <= 9) {
    return 'Password must be at least nine characters';
  }
  return '';
}
function validateEmailAddress(emailAddress) {
  var re = new RegExp("[a-z0-9!#$%&'*+/=?^_{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_{|}~-]+)*@@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?");
  if (!re.test(emailAddress)) {
    return 'Email Address is not valid';
  }
  return '';
}
function validateZipCode(zipCode) {
  var re = new RegExp("^\\d{5}(-\\d{4})?$");
  if (!re.test(zipCode)) {
    return 'Zip Code is not valid';
  }
  return '';
}

The next logical step is to break out the code that submits the form. This will ensure the final business rule, if the user data is not valid then do not submit the form can be covered by unit tests.

function submitUser (user) { 
  var errorMessage = validateUser(user); 
  if (!errorMessage || errorMessage.length === 0) { 
    errorMessage = sendUserToRestService(user); } return errorMessage; }

function sendUserToRestService(user) { 
  var errorMessage = ''; 
  $.ajax({ 
    url: 'http://jsfiddle.net/echo/json/', 
    type: 'POST', 
    dataType: 'json', 
    data: user, 
    success: function () { 
      errorMessage = ''; 
    }, 
    error: function (message) { 
      errorMessage = 'Error' + message; 
    } 
  }); 
  
  return errorMessage; 
}

As you can see all the UI code has been pretty much isolated away from all the business logic.

function submitUserForm() {
  var user = {
    name: $('#name').val(),
    password: $('#password').val(),
    emailAddress: $('#emailAddress').val(),
    zipCode: $('#zipCode').val()
  };
  $('#message').hide();
  var errorMessage = submitUser(user);
  if (errorMessage.length !== 0) {
    $('#message').html(errorMessage);
    $('#message').show();
  }
}

Finally, lets wrap it all up with the unit tests to ensure all the business rules are being followed.

describe('validator', function() {
  it('validateUser_nameIsEmpty_errorMessageReturned', function () {
    var user = { name: '' };
    var results = validateUser(user);
    expect(results).toContain("Name");
  });

  it('validateUser_nameIsNull_errorMessageReturned', function () {
    var user = { name: null };
    var results = validateUser(user);
    expect(results).toContain("Name");
  });

  it('validateUser_nameIsNotEmpty_noErrorMessageReturned', function () {
    var user = { name: 'test' };
    var results = validateUser(user);
    expect(results).not.toContain("Name");
  });

  it('validateUser_passwordIsEmpty_errorMessageReturned', function () {
    var user = { password: '' };
    var results = validateUser(user);
    expect(results).toContain("Password");
  });

  it('validateUser_passwordIsNull_errorMessageReturned', function () {
    var user = { password: null };
    var results = validateUser(user);
    expect(results).toContain("Password");
  });
  it('validateUser_passwordIsNotEmptyButLessThan9Characters_errorMessageReturned', function () {
    var user = { password: 'test' };
    var results = validateUser(user);
    expect(results).toContain("Password");
  });
  it('validateUser_passwordIsNotEmptyAndGreaterThan9Characters_noErrorMessageReturned', function () {
    var user = { password: '1234567890' };
    var results = validateUser(user);
    expect(results).not.toContain("Password");
  });

  it('validateUser_emailAddressIsEmpty_errorMessageReturned', function () {
    var user = { emailAddress: '' };
    var results = validateUser(user);
    expect(results).toContain("Email Address");
  });

  it('validateUser_emailAddressIsNull_errorMessageReturned', function () {
    var user = { emailAddress: null };
    var results = validateUser(user);
    expect(results).toContain("Email Address");
  });

  it('validateUser_emailAddressIsNotValid_errorMessageReturned', function () {
    var user = { emailAddress: 'b.com' };
    var results = validateUser(user);
    expect(results).toContain("Email Address");
  });

  it('validateUser_emailAddressIsValid_noErrorMessageReturned', function () {
      var user = { emailAddress: 'test@test.com' };
      var results = validateUser(user);
      expect(results).not.toContain("Email Address");
    });

  it('validateUser_zipCodeIsEmpty_errorMessageReturned', function () {
    var user = { zipCode: '' };
    var results = validateUser(user);
    expect(results).toContain("Zip Code");
  });

    it('validateUser_zipCodeIsNull_errorMessageReturned', function () {
      var user = { zipCode: '' };
      var results = validateUser(user);
      expect(results).toContain("Zip Code");
    });

    it('validateUser_zipCodeIsNotValid_errorMessageReturned', function () {
      var user = { zipCode: 'a' };
      var results = validateUser(user);
      expect(results).toContain("Zip Code");
    });

    it('validateUser_zipCodeIsValid_noErrorMessageReturned', function () {
      var user = { zipCode: '68137' };
      var results = validateUser(user);
      expect(results).not.toContain("Zip Code");
      });
});


describe('submitUser', function () {
  it('submitUser_userIsNotValid_postIsNotSent', function () {
      var user = {};
        spyOn($, 'ajax');
        submitLesson1User(user);
        expect($.ajax).not.toHaveBeenCalled();
  });
    it('submitUser_userIsValidAndSubmissionSuccessful_noErrorMessageReturned', function () {
      var user = {
        name: 'Test',
        emailAddress: 'test@test.com',
        password: 'abcd1234599',
        zipCode: '68137'
      };
      spyOn($, 'ajax').andCallFake(function (request) {
        request.success();
      });
        var results = submitLesson1User(user);
        expect(results).toEqual('');
    });

    it('submitUser_userIsValidAndSubmissionNotSuccessful_errorMessageReturned', function () {
      var user = {
        name: 'Test',
        emailAddress: 'test@test.com',
        password: 'abcd1234599',
        zipCode: '68137'
      };

      spyOn($, 'ajax').andCallFake(function (request) {
        request.error('error');
      });

      var results = submitLesson1User(user);
        expect(results).toContain('error');
  });
});