There are good reasons why programmers perform tests themselves in contrast to letting an external testing department (QA) perform the tests. The programmer is able to discover errors in code and in general design decisions much faster and can react immediately, as one might say, the feedback loop is very short. On the other hand, when programmers have both, the test and the implementation in mind, they’re tempted to test code that an external wouldn’t test, simply because the external only know about the spec but not the implementation. That way I once wrote way more tests than needed and at the time of writing the tests felt confident with that approach because more tests are better than less tests and JavaScripts lacking of private members literally encouraged me to do this anyway. Well, I paid the price some weeks later, when a minor change took me way to long to implement because of some dozens of failing tests that shouldn’t be affected by my change. But what exactly is bad about testing other things than public API?

Consider the following little code snippet:

function Auth() {}

Auth.prototype._isEmpty = function(str) {
  return 0 === str.length;
}

Auth.prototype.login = function (username , password) {
  if (this._isEmpty(username) || this._isEmpty(password)) {
    return false;
  } else {
    return true;
  }
}

What is public API?

First of all, make yourself aware of what methods of your Auth class are meant to be accessible and visible to the user of the class. That’s the public API. In this case the only method that is supposed to be visible is:

boolean function login(username:String, password:String)

What are implementation details?

Now that you have settled the public API, everything else can be considered as implementation detail, including the _isEmpty method. I use a single underscore prefix to make the distinction more visible. But that’s only a coding convention and not a JavaScript feature and therefore doesn’t prevent a tester from using it per se. It can help code-reviewers with detecting private members though.

Tip: A user of your class Auth only cares about public API because that’s the only API that is well documented and maintained. So let’s make sure that this API works as expected and only test login. Applying this approach keeps the amount of tests as small as possible but effective as necessary.

A bad test

describe('Login', function() {
  describe('Login', function() {
    it("calls _isEmpty", function () {
      var auth = new Auth();
      spyOn(auth, '_isEmpty');
      auth.login('abc', 'abc');
      expect(auth._isEmpty).toHaveBeenCalledWith('abc', 'abc');
    });
  });
});

So what makes this test bad? The test expects a method, that is private and not visible to the outerworld, to be available and callable. Which means as soon as we gonna refactor _isEmpty for internal, performance or whatever reasons the test fails. This slows down your productivity and makes refactoring hard because you’re forced to align the test every time when you’re touching _isEmpty. Does this test give you any confidence that your public API is working as expected? No. Calling _isEmpty doesn’t give any infos about whether login returns true or false. Does it improve your public API? No, not at all. The user of this class doesn’t even care about whether _isEmpty exists or not as long as login works. As a result changing internal code will let this test fail unexpectedly.

Testing Terminology: A test that fails, although the code changes shouldn’t have affected the test are often called: Fragile Test

Good tests

describe('Login', function() {
  it('returns `true` when username and password are not empty', function() {
    expect(new Auth().login('non-empty', 'non-empty')).toBe(true);
  });

  it('returns `false` when username is empty', function() {
    expect(new Auth().login('', 'non-empty')).toBe(false);
  });

  it('returns `false` when password is empty', function() {
    expect(new Auth().login('non-empty', '')).toBe(false);
  });
});

Focus on simplicity, correctness and shortness of tests. You and your co-workers will enjoy the readability. Internal documentation meant for developers can be replaced by your tests. From now on your tests are the spec. What about refactoring? Super easy. Let’s say you replace _isEmpty for internal reasons by underscrore.isEmpty() and some days later by lodash.isEmpty(). No problem, you don’t even need to touch the tests. Just execute them and give yourself the confidence that login still works as expected. This process doesn’t only make refactoring easy, it encourages you to make your code continually better and let you react fast on internal design decisions. Code-reviews of refactoring steps can happen faster because one thing is sure, whatever your code does, it doesn’t break the public API because you didn’t touch the tests.

Summary

KISS! Testing less but the right things improves your development process and makes you and your co-workers happy. Implementation details are never mentioned within the spec, so they never appear within your tests.