This part of the article will cover the impact of having effective workflows during our development sessions as well as how proper test code quality enables us to create maintainable tests, especially for complex projects.
Development workflows & pipelines
Programming is a flow activity and we developers should be interested in keeping our workflow efficient and the turnaround times low, in order to not get distracted by waiting times.
In general, we want to make sure that the overall run time of our tests doesn’t exceed a few seconds, at least for all tests that we execute during our “change-verify-repeat” cycle. During development, it’s crucial to have a quick turnaround for this cycle which spans from making some changes in the project to verifying that the changes work as expected in a production-like setting.
It would be too slow if we would do this verification only through a pipeline that builds and deploys our application from scratch each and every time — no matter whether that runs locally or not. For this reason, it’s advisable to create a development setup that listens for file changes, then modifies the applications to reflect our code changes, by redeploying the whole application or by updating single classes or files, and then quickly re-executes tests scenarios that connect to the updated application-under-test. The idea is, that it’s usually much faster to only redeploy the application or parts of it, instead of starting up the processes from scratch, and keep potential other local services running. This approach makes running system tests locally even more interesting, since we can re-execute the test scenarios against the running system, and instantly verify our changes.
More specifically, a local workflow can be to: recompile the classes, execute unit test, (hot-)deploy the application, and execute the idempotent integration tests. This whole cycle should not exceed much more than one or two seconds, otherwise our attention wanders elsewhere. We can potentially split integration tests, due to their startup and execution time, system tests that take longer, or any other more complex scenarios, to a separate test suite that is executed less frequently or only runs in the CI/CD pipeline.
Test code quality & maintainable tests
The biggest issue that leads to insufficient testing in projects is the lack of maintainable test code. In many cases, tests are written, or rather copy-and-pasted, in a way that makes is very hard to change them once a bigger change in the production code is made. That means, test code is typically treated with less attention to its quality. That goes well as long as there are only very few test cases. However, what you typically see in projects is that as the code base grows more complex, the test suite becomes less and less maintainable, just like our production code, if we wouldn’t apply refactoring.
That’s the main point in having maintainable tests: applying the same code quality principles, especially separating concerns and introducing abstraction layers. It’s possible and highly advisable to create re-usable components within your test scope, if care is taken not to introduce leaky abstractions.
Let’s look at an example that illustrates this further. We start with what I sometimes call “comment-first programming”, where we write in code comments, pseudo code, or even on paper, what our test scenarios should verify, on a purely conceptual, business-logical level. For example, “create an Espresso coffee order with size large”. Or “verify that the order is in the system with type Espresso and size large” That’s it. How that order is created is not a part of this level but implemented in a lower abstraction, a separate method, or typically separate delegate. The same is true for low-level verification, for example, to check whether the correct HTTP status code and expected JSON structure has been sent back. We should take care not to leak information that is only relevant on the detailed level, such as JSON structure, to the delegating method.
You can have a look at this video to see an example flow how that is implemented. From a tester’s or domain expert’s perspective this approach makes a lot of sense, since we’re start which what scenarios we want to test, not how they are implemented. If the implementation changes, e.g. the communication with the systems changes from HTTP to something else, only a single place needs to be adapted. Furthermore, our test case methods become very readable, since they precisely express the idea of what is tested; if we’re interested in the how, we’ll find that in the lower level.
For example, the following system tests verifies the creation of a coffee order:
Even a non-technical domain expert could, without having knowledge about Java, understand what that test scenario is executing, if they know the domain behind orders, coffee types, and origins, and if they’re comfortable with ignoring the Java-specific syntax.
This is the reason why I claim it’s more important to focus on test code patterns rather that specific test frameworks. For real-world projects it’s crucial to introduce proper test code quality, mostly crafting abstraction layers and separating concerns into delegates. Especially when the projects grow more complex, this difference shows very quickly.
The next part of the article series will cover test frameworks and when to apply them.
Published on Java Code Geeks with permission by Sebastian Daschner, partner at our JCG program. See the original article here: Efficient enterprise testing — workflows & code quality (4/6)
Opinions expressed by Java Code Geeks contributors are their own.