Testing - It's about confidence

01 Sep 2017. comments

Lets talk about testing. Its fairly well accepted that you should be writing tests, and that more tests are better than less tests, and that writing tests first can be very helpful. But often times new people to the field aren’t really sure why they are writing tests, or how to write good tests, or why certain practices are desirable while others are not. Lets dive in.

Tests are for humans​ and tell a story

Tests are for humans to gain confidence in the code they have written.​ Anything you can do to your tests so that they give the team more confidence is a win.​

Being boring, obvious, simple and clear is a great way to gain confidence.​ Make tests easy to read as though they were telling a story about your feature (because that is what they are doing).​

Cleanly separate test setup with test implementation

To see how we can tell a story (or not tell a story), take a look at this code:

describe('save', function() {

  it('storage', function() {
    webStorage.save.mockReturnValue({ alpha: 10 });
    const persistence = new Persistence();
    expect(persistence.isEmpty).toBe(true);
    expect(persistence.books.length).toBe(0);
    persistence.save('books', { alpha: 10 });
    expect(persistence.books.length).toBe(1);
    expect(persistence.isEmpty).toBe(false);
    expect(persistence.books.length).toBe(1);
    expect(webStorage.save).toHaveBeenCalledWith({
      type: 'books',
      record: { alpha: 10 }
    });
  });

});

It isn’t clear without reading carefully through the code what this is testing.​ Its tough for us humans to see what part of this is test setup vs test.​ When this test fails we will need to rely on a stack trace to identify what expectation failed (what thing being tested has failed?). ​

We’ll see how to clean this test up shortly.

Given, When, Then

A good test should be quite obvious about:​

  • What scenario is being tested​
  • The thing that is under test​
  • The expected outcome​

Martin Fowler calls this “Given, When, Then”​.

Others call it “Arrange, Assert, Act”​:

​ Looking back at our previous test, what this means is that we need to do the following:

  • We need a ‘describe’ for the scenario that we are testing.
  • We need that ‘describe’ to be married to a ‘beforeEach’ that translates that plain-english scenario into code that sets it up.
  • We need an ‘it’ block that plain-english describes our expectation.
  • The ‘it’ block needs to then have code that asserts only that expectation alone. ​

This gives us a clean separation of​:

  • “this is my scenario” (describe) (Given) (Arrange)
  • “this sets it up” (beforeEach) ​(When) (Act)
  • “this is my expectation” (it) (Then) (Assert)

Lets re-create our test under those guidelines:

describe('Persistence', function() {

  let persistence;

  beforeEach(function() {
    persistence = new Persistence();
  });

  describe('isEmpty()', function() {

    describe('when no records have been saved', function() {

      it('returns true', function() {
        expect(persistence.isEmpty).toBe(true);
      });

    });

    describe('when we save a record', function() {

      beforeEach(function() {
        webStorage.save.mockReturnValue({ alpha: 10 });

        persistence.save('books', { alpha: 10 });
      });

      it('returns false', function() {
        expect(persistence.isEmpty).toBe(false);
      });

    });

  });

  describe('save()', function() {

    describe('given a book to save', function() {

      let toSave;

      beforeEach(function() {
        toSave = { alpha: 10 };

        persistence.save(toSave);
      });

      it('stores the book correctly in persistence', function() {
        expect(webStorage.save).toHaveBeenCalledWith({
          type: 'books',
          record: toSave
        });
      });

    });

  });

});

We can clearly see our “Given, When, Then” showing up here.​

  • Given a persistence
  • When I dont save any records
  • It is empty

And

  • Given a persistence
  • When I store a record
  • It is not empty

And

  • Given a persistence and a book
  • When I store call save() with the book record
  • It stores the book correctly in persistence

Its clear from the outer descriptions what we’re testing.​ ​In each case we know what scenario we setup by looking at the describe/beforeEach pairing (plain english + code to make it happen).​ ​And its is very easy to see what our expectation is because it isn’t mixed in with setup code.​ And in the case where the “expect” might not be clear to everyone, we have a plain-english documentation of what the expectation is checking.

Additional Benefits

Beyond making it read clearer to humans (arguably the most important part), how does this structure help us?​ Well, adding another test for a given scenario (that has already been arranged) is just a matter of adding another it/expect. None of the other tests in the scenario must change, and we can be confident that the existing scenario/expectations are still proving our code works because we didn’t need to change them (which would decrease our confidence).

Confidence

The purpose of tests is to give you confidence.​ Tests are the wall at your back that allow you to move forward confidently when making changes.​ Ease-of-understanding breeds confidence.​ Testing is not just a box to tick during development; they are there to help you.​ And more importantly, they are there to help FUTURE YOU.​

Don’t change your implementation in lock-step​​!

You should be free to refactor the internals of a thing under test and use the unchanging tests to validate that it still works the way it did before.​ ​If you have to change your tests when you change implementation details (private functions) your tests are not adding value and just be come a box to tick.​

Mocking/Faking/Spying can help, but beware

Mocking is both a powerful tool and a dangerously seductive one. It can be easy to mock many things to get your test to be green, but that can often fool you into believing your implementation is working when really you’ve just overmocked it to death. So how do you know when is enough vs too-much?

  • If you have to change your tests in response to having changed internal private methods or structure of the file under test, you’ve probably over-mocked.
  • If you’re mocking a collaborator of a collaborator, you’ve probably over-mocked.
  • If your test is hitting I/O, you’ve under-mocked.

Additional Resources

The following are great resources to expand on how to structure your tests.​

BetterSpecs - Guidance for ruby rspec tests, but guidance still applicable for other frameworks and languages:​

TestDouble’s wiki on how to write good tests:​

comments

Tagged: testing tests unit testing mocking mocks test driven development tdd

2017 Ben Lakey

The words here do not reflect those of my employer.