I'm not expert on this topic, but it's an important topic that shouldn't go unanswered.
In my experience, writing tests and testable code needs quite some experience - it's not something you only learn from reading.
Some things to learn:
- Test framework
- Assers
- Mocking
- Coverage
- Reporting
- Quality gates on commits (tests are only useful if they're run)
- Are tests based on design/documentation?
- Conventions
- How much coverage do you want/need? (lines or paths?)
- Do you test private functions?
- Do you write tests before (TDD) or after?
- Ate tests written by the developer (more efficient but biased)?
- Which functionality is worth testing
- Is it hard to test automatically?
- Is it likely to break?
- How serious is it if it breaks?
- (Can it be a compile/lint error instead? Because that'd be preferable).
- Write testable functions
- Allow mocking of prerequisites (by loose coupling with dependencies)
- ...but don't go overboard with the dependency injection - you can't pass every dependency all the way from the main function and not everything needs an abstract factory pattern
- Prevent side effects - make required state and expected results explicit (I prefer arguments and return values over changing state somewhere).
- Try to isolate things like database access so it's easier to mock.
Writing testable code is really hard. Especially if there is a lot of database access or api calling going on.
The upside is that many qualities that make code testable also make it maintainable in general, especially loose coupling and small functions with a single responsibility.