Our tests should:
- Assure us that the code is behaving correctly.
- Allow us to refactor with confidence.
- Help us write more maintainable code.
- Choose the part of the code you want to test (a class, a group of classes, the full end to end stack). For functional tests, try and pick a group that is at the same "level", or that achieve an isolated unit of work that makes sense as a standalone function.
- Draw a mental box around the classes that will be in the test. Everything inside that box should be able to be completely refactored (methods renamed, classes renamed, code moved around) without breaking this test. Only mock or stub or test calls to things at the edges of the box.
- In a unit test, the boundaries of the box are the class boundaries. Mock and stub things that this class calls out to. Don't stub or test any private methods, as this would mean that the test would break if you refactored.
- In a functional test, the boundaries of the box are the places where your chosen group of classes makes calls to classes that are not within your chosen group. Mock or stub the calls to these outside classes. You should be able to completely refactor any code within the bondaries of this box without this test breaking.
- Some calls will be queries (they return a thing) - make assertions on the results. Some calls will be commands (they cause a side effect) - test the side effects. Some calls will be both (though this is not an ideal case) so you'll need to test the side effects and the results. Testing a side effect when you are stubbing/mocking involves writing an assertion that the correct call was made to the class at the boundary. Testing a side effect when you are not subbing/mocking involves checking that the desired output is present (eg. a record in a database). When chosing between mocking and not mocking, chose the one which will be less painful (less code, less brittle, less likely to give you false-positives or false-negative, faster running) in the long run.
- The more classes that are being covered by the test, the less strict the assertions should be. Firstly, we don't want to duplicate our tests - ideally, if one piece of logic changes, as close to one test as possible should fail. Secondly, the more code that is being covered by a test, the more opportunity there is for small changes to make the tests break. We don't want tests that break often every time there is a small change - this is called being "brittle". This is unhelpful as we tend to see a whole heap of tests breaking, and then go and just update the tests mindlessly, meaning we might miss a genuine bug.
- A unit test could check that an email is being created with the correct subject, to, from, body and is being sent.
- A functional test of a component (a group of classes) could just check that an email is being sent.