On Testability
Acceptance testing is basically black-box testing done by the QA team to sign off. It’s very important to have clear pre-defined goals for acceptance testing as early as possible during the planning phase. In fact, the requirements specifications should define negative scenarios and expected failure behavior, not just positive scenarios and use cases. These requirements should be mapped accordingly to unit tests in the design specifications and functional test cases in the test specifications. The unit tests specifications could be used as an input to create the design of the system, given that it covers all positive and negative scenarios including mainstream and corner cases. This methodology is analogous to Test Driven Development (TDD) when you write test cases first. Thinking about testability helps you have a better design because you were thinking about test cases first, on the contrary to when the test planning occurs after the system is designed. Basically, you’re trying to think about what could go wrong, so that you can avoid it.
Automation should be your goal. Fully automated test suite help to prevent regression and introduction of new defects. It also gives you a level of confidence in the new changes made, given that you already have good code coverage. If you think about it, new pieces of code should be covered 100% by accompanying test cases, because you don’t want to check in code that’s unreachable by tests or any excess code that’s unnecessary. Hence, you can see that because you thought about testability first, it lead you to a good design. Untestable code is bad code! The following are characteristics of untestable code:
- Unreachable from test code: Code that can’t be exercised from test cases. For example, exception handling code for exceptions that are hard to reproduce in test code. The solution is to refactor that code into a method that’s reachable by test code:
1: try 2: { 3: client = server.Accept(); 4: } 5: catch (SocketException ex) 6: { 7: HandleException(ex); 8: }
- Adding business logic to the presentation layer: UI tests are hard to implement, flaky, and unstable. The Model-View-Controller (MVC) pattern helps make the code testable because the presentation layer become very thin, hence you can write your tests against the model and the controller layers. Also, it makes the design more resilient to UI changes, you can replace that layer easily whenever you want to.
- Dependency on other subsystems: If you tight coupling with another subsystem like running SQL queries within your business layer or dependency on the network, or the file system, etc. Separate concerns, refactor code, and isolate layers so that you can mock these subsystems.
- Concrete classes: The more complicated a class is, the more testing it needs. However, this also correlates to how complicated it’s to construct an testable instance of it. Usually, its constructor and its methods need to be passed instances of other complicated classes, which in turn, depend on other classes, and so on. If you use interfaces (or abstract classes) for these parameters, you can always mock.