There is an asymmetry that is interesting for people familiar with cryptography: as a developer, you can (re)create the production code if you have your test cases, but you cannot easily create the tests from your production code. So, the tests are a kind of collection of one-way functions — a trapdoor function; the implication is that the tests are perhaps more important than the production code itself.
There is also another asymmetry for people who have had to debug code (which is every developer in the world): developers using Test-Driven Development on a new project typically find that they tend to invoke a debugger less often than on a project without a test-first approach. Test-driven development, with robust version control, enables developers to revert back to the last version that passed all tests, and can be sometimes more productive than having to debug poor quality code.
With that preamble, let us delve into the practices of Test-Driven Development (TDD), and why we have built out the support for TDD in the Neo Blockchain Toolkit.
Testing does not have to dogmatically happen after the implementation; with TDD, testing goes first, reversing the legacy approach of developing the software and then testing it (typically, parts of it).
Why? The approach of writing a test that verifies the behavior of the expected feature, and then to write the code that implements the feature has two key implications; first, the test(s) are used as a compass to direct the developer to focus on the expected outcomes, and second, the tests are used a tracker to indicate how close the code to being completed.
What else? Further, with this approach, the developer then has a test or a test suite that can be repeatedly run to ensure that this feature and its behavior and outcomes work as expected even as other unrelated changes are made to the code and other new features are added to the system over time.
And for the business owner? Test-driven development encourages and nudges the developer to develop the application one ‘small’ feature at a time, taking ‘small’ steps from one stable and functional release to another. The early and frequent application of the test cases facilitates the identification and resolution of bugs early in the cycle, preventing them from becoming expensive business problems later.
TDD emphasizes a focus on simplicity with the dictum that the developer writes the simplest possible code to pass a unit test; write the unit test, watch it fail, and then make code changes until it passes.
What is a unit test? It is usually a “small” test i.e., low-level, and focusing on a “small” part (code) of the overall application. The result of running a unit test is binary: a “pass” if the code’s performance is consistent with the documented expectations, and a ““failure”” otherwise. Further, a unit test should be “deterministic” i.e., it should not have side-effects.
There are typically five steps that exemplify TDD approaches.
First, the requirements for the feature (or bug): an understanding of the initial requirements, but not requiring excessive documentation in this step, as TDD will help to refine the details later in the cycle.
Second, the unit test: as described earlier, usually a brief piece of code, written by the developer of the application, and with the purpose of exercising a limited portion of the target application’s codebase.
Third, the implementation: write the code that attempts to satisfy the requirement(s), and then run the unit test. Repeat this step until the code passes the unit test.
Fourth, the refactoring of the implementation: “improving” the innards of the existing code, without modifying its external behavior, to simplify the internal design and to increase code comprehension.
Finally: the repetition of this sequence as the requirements are augmented: each cycle will be short, with rapid cycles, enabling safer refactoring towards delivering the highest quality production code.
For this cycle to be effective, the unit tests should be able to exercise the behavior of the code that is being tested, without depending on or being influenced by other unrelated code; “Mock” objects are typically used to facilitate this in the real world (think decentralized Oracles). Mock objects are powerful abstractions; in practice, mocking is most effective across significant architectural boundaries.
Blockchain application development poses unique challenges with respect to Test-Driven Development. What distinguishes decentralized application platforms are also what makes them ill-suited to TDD by default. The Neo Blockchain Toolkit addresses these head-on and raises the bar in addressing the needs of the professional programmer. In no particular order, a few of these are highlighted below.
Microsoft uses the term “inner loop” to describe the iterative process that a developer performs when he or she writes, builds, and debugs code. Every developer and development team’s inner loop will differ based on the tools that they use, the stack that they work on, and the individual developer’s preferences.
With the new Offline mode support in Neo Express for Neo3, the Neo Blockchain Toolkit serves to optimize the developer “inner loop” and enables the developer to reset their PrivateNet, redeploy their Smart Contracts and to rebuild their checkpoints in a matter of seconds, fast enough to be included in the automated build process.
The Arrange-Act-Assert pattern is a well-known way of writing unit tests for a function that is being tested.
· The Arrange section of a unit test method does the initialization; arranging the initial preconditions and the input values that are passed to the function under test
· The Act section invokes the function under test with the arranged inputs.
· The Assert section “asserts” (verifies) that the function performs as expected and that the results match the expectations
With blockchain application development, the “Arrange” section of this pattern is non-trivial; again, what differentiates decentralized stacks is what complicates the setup for testing. The Neo Blockchain Toolkit makes this set of tasks rather trivial and straightforward to setup with the innovative application of the checkpoint capabilities in the PrivateNet.
The Neo protocol led the industry with its polyglot approach to development; programmers could for the first time use a variety of languages (C#, Python, Go et al). The Neo Blockchain Toolkit builds on this and the test framework that ships with the Toolkit may be used to write tests for contracts in the tester’s choice of language.
This, again, lowers the barriers to enable a larger community of developers to contribute and build the test suite.
Test-driven development significantly shapes and influences the architecture of the application.
First, the focus on the test cases forces the developer to understand and empathize with the customer or as the case may be, another developer; the developer must precisely document and then subsequently comply with the service and component interfaces. Similar to how design by contract approaches use assertions, TDD forces the developer to approach the code with comparable discipline.
Second, Test-first typically leads to enhanced code modularization and extensibility because the TDD practices require the developer to think in terms of “small” units that can be implemented and tested independently and then subsequently integrated and deployed together. This results in higher cohesion i.e., the behaviors and functionality within a service or component belong together and have focus.
Third, the mock object design pattern requires the loose coupling of services and components for the purposes of testing and nudges the developer to enforce architectural boundaries using interfaces. This promotes both code/component modularization since components require the ability to switched easily between across (mock) instances for TestNet, PrivateNet and the MainNet instances for deployment.
The ideas behind modularization, loose coupling and the separation of concerns et al have long been foundational pillars of “good” architecture; that TDD reinforces these concepts is perhaps a validation of the adage that “good” architectures are easily testable and vice versa.
In practical terms, it is a simple proposition: write production code in order to make a failing test case pass, the net result being that the developer has to write the test cases first. However, the outcomes with respect to the architecture of the application, the design of the services and components, the on-going activity of refactoring to patterns, and the quality of the production code are significant.
Now, with the Neo Blockchain Toolkit, developers can take full advantage of Test-Driven Development techniques as they build their applications.
Author: John deVadoss