4
\$\begingroup\$

I have a simple web page where the user enters the name and rank of a soldier and hits submit. If the input is invalid, an appropriate error message gets displayed. Otherwise, it adds the soldier to a database and adds it to a list of current Soldiers via AJAX request.

//Find elements on page
var rankField = $("input[name='rank']");
var nameField = $("input[name='name']");
var submit = $("input[type='submit']");
var list = $("ul");

//attach event-handler to submit button
submit.click(function() {
  var rank = rankField.val();
  var name = nameField.val();
  onSoldierSubmitted(rank, name);
});

function onSoldierSubmitted(rank, name) {
  clearErrors();

  var rankIsValid = isValidRank(rank);
  var hasErrors = false;

  if (!rankIsValid) {
    addError('rank is invalid');
    hasErrors = true;
  };

  if (!name) {
    addError('Please enter a name for your Soldier');
    hasErrors = true;
  };

  if (hasErrors) {return;}
  appendSoldier();
};

function clearErrors() {
  $('.alert-danger').empty();
};

function addError(errorText) {
  var errorDiv = $('.alert-danger');
  var currentError = errorDiv.html();

  if (currentError) {
    currentError += '<br>';
  };

  errorDiv.html(currentError + errorText);
};

function isValidRank(rank) {
  var ranks = ['GEN', 'LTG', 'MG', 'BG', 'COL', 'LTC', 'MAJ',
      'CPT', '1LT', '2LT', 'CW5', 'CW4', 'CW3', 'CW2', 'WO1',
      'SMA', 'CSM', 'SGM', '1SG', 'MSG', 'SFC', 'SSG', 'SGT',
      'CPL', 'SPC', 'PFC', 'PV2', 'PVT']
  normalizedRank = rank.toUpperCase();
  return ranks.indexOf(normalizedRank) != -1;
};

function appendSoldier() {
  $.get("/ajax/soldier", 
  {
    rank: rankField.val(),
    name: nameField.val(),
  }).done( 
    function(data) {
      soldiers = extractSoldiersFromList(data);
      renderHTMLListFromData(soldiers);
    }
  );

  clearInput();
};

function extractSoldiersFromList(data) {
  var soldierData = $.parseJSON(data);
  var soldiers = [];

  for (var i=0; i < soldierData.length; ++i) {
    var soldier = soldierToString(soldierData[i]);
    soldiers[i] = soldier;
  }

  return soldiers;
};

function renderHTMLListFromData(data) {
  list.empty();
  elements = data;
  addSoldiersToUL(elements);
}

function addSoldiersToUL(soldiers) {
  for (var i = 0; i < soldiers.length; ++i) {
    list.append("<li>" + soldiers[i] + "</li>");
  }
}

function clearInput() {
  rankField.val('');
  nameField.val('');
}



function soldierToString(soldierDict) {
  var rank = soldierDict.rank;
  var name = soldierDict.name;

  return rank + " " + name;
};

(Yes, I realize I am a horrible person for using GET instead of POST for this. I was having trouble getting POST to work with my back-end.)

I'm having trouble figuring out the right unit tests for the appendSoldier function:

test("appendSoldier makes request with correct args", function() {
  sinon.spy($, 'get');
  rankField.val('PVT');
  nameField.val('Snuffy');

  appendSoldier();

  ok($.get.calledWith("/ajax/soldier", {rank: 'PVT', name: 'Snuffy'}));
  $.get.restore();
});

test("UL of Soldiers populates correctly", function() {
  renderHTMLListFromData(soldierData);
  ok(list.has('li').text('PVT Snuffy'));
});

My reasoning here is that unit tests aren't supposed to test anything external, so I make sure the right request gets called, and that the proper thing is done with the response. I'm unhappy with this, because it still doesn't tell me that appendSoldier works like it's supposed to. I guess that's just something I leave to my functional tests?

I'm also not sure about my tests for the onSoldierSubmitted function, which displays appropriate errors, or calls appendSoldier if there aren't any errors.

Here is my test for valid input:

test("Get request called with valid input", function() {
  sinon.spy($, "get");
  onSoldierSubmitted('pvt', 'snuffy');
  ok($.get.called, "method called");
  $.get.restore();
});

I'm not sure whether I ought to be uncomfortable that this tests that appendSoldier sends the get request, rather than testing that appendSoldier gets called.

\$\endgroup\$
2
\$\begingroup\$

My reasoning here is that unit tests aren't supposed to test anything external

I agree with that. You can't test external things like an AJAX call anyway.

I'm unhappy with this, because it still doesn't tell me that appendSoldier works like it's supposed to.

Actually, you CAN test the appendSoldier method. You need to refactor it in the following way:

function appendSoldier() {
    $.get("/ajax/soldier", 
    {
       rank: rankField.val(),
       name: nameField.val(),
    }).done( 
        function(data) {
            // Notice that I extracted these 2 lines of code into a separate method
            // soldiers = extractSoldiersFromList(data);
            // renderHTMLListFromData(soldiers);
            appendSoldierSuccess(data);
        }
    );

    clearInput();
};

// This is the extracted method
function appendSoldierSuccess(data) {
    soldiers = extractSoldiersFromList(data);
    renderHTMLListFromData(soldiers);
}

Now you can unit test the functionality in the following way (I will use Jasmine because I'm familiar with it but the concepts can be mapped to Sinon):

it("appendSoldierSuccess()", function() {
    var data = {};
    var soldiers = {};

    spyOn(window, 'extractSoldiersFromList').and.returnValue(soldiers);
    spyOn(window, 'renderHTMLListFromData');

    appendSoldierSuccess(data);

    expect(window.extractSoldiersFromList).toHaveBeenCalledWith(data);
    expect(window.renderHTMLListFromData).toHaveBeenCalledWith(soldiers);
});
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.