Test-driven development (TDD) is a software development approach that prioritizes writing automated tests while creating the actual code. There follows a cycle of writing a failed test, writing the code to make the test pass, and then refactoring the code. TDD was originally developed to ensure the quality, maintainability and expandability of the software created over the long term. The specific knowledge about the individual source text passages should also be shown in the tests. Thus, a transfer of responsibility between developers is supported. Better than any documentation, tests are always up-to-date regarding the function that has been implemented in the source code.
However, TDD also has a positive impact on the security of a program. And that’s what we’re going to look at now.
But first, let’s get back to TDD. There are a variety of theoretical approaches here. I will briefly touch on them here to give an overview. The question to be addressed is whether the TDD approach influences the effect.
Classic TDD – Kent Beck
Unit tests and units tested with them are continuously developed in parallel. The actual programming is done in small, repeating micro-iterations. One such iteration, which should only take a few minutes, consists of three main parts, known as the red-green refactoring.
Red: Write a test to test a new behaviour (functionality) to be programmed. You start with the simplest example. If the feature is older, it could also be a known bug or new functionality to be implemented. This test is initially not fulfilled by the existing program code, so it must fail.
Green: Change the program code with as little effort as possible and add to it until it passes all tests after the test run then initiated.
Then clean up the code (refactoring): remove repetitions (code duplication), abstract if necessary, align with the binding code conventions, etc. In this phase, no new behaviour may be introduced that would not already be covered by tests. After each change, the tests are performed; if they fail, it is impossible to replicate what appears to be a bug in the code being used. Cleanup aims to make the code simple and easy to understand.
These three steps are repeated until the known bugs are fixed, the code provides the desired functionality, and the developer can’t think of any more useful tests that could fail. The program-technical unit (unit) treated in this way is considered complete for now. However, the tests created together with her are retained to be able to test again after future changes as to whether the behavioural aspects that have already been achieved are still fulfilled.
For the changes in step 2 – also called transformations – to lead to the goal, each change must lead to a more general solution; for example, she may not only edit the current test case at the expense of others. Tests that get more and more detailed bring the code to a more and more general solution. Regular attention to transformation priorities leads to more efficient algorithms.
Consistently following this approach is an evolutionary design method, where every change evolves the system.
Outside in TDD
Outside-In Test-Driven Development (TDD) is an approach to software development that places emphasis on starting the development process first by creating high-level acceptance tests or end-to-end tests that demonstrate the desired behaviour of the system from his point of view to define users or external interfaces. It is also commonly referred to as behaviour-directed development (BDD).
With Outside-In TDD, the development process begins with writing a failed acceptance test that describes the desired behaviour of the system. This test is usually written from a user’s perspective or a high-level component that interacts with the system. The test is expected to initially fail as the system does not have the required functionality.
Once the first acceptance test has been performed, the next step is to write a failing unit test for the smallest possible unit of code that will pass the acceptance test. This unit test defines the desired behaviour of a specific module or component within the system. The unit test fails because the corresponding code still needs to be implemented.
The next step is implementing the code to make the failed unit test succeed. The code is written incrementally, focusing on the immediate needs of the failed test. The developer writes further tests and code step by step until the acceptance test is passed and the desired behaviour of the system is achieved.
The idea behind Outside-In TDD is to drive the development process from the outside, starting with the higher-level behaviour and moving inward to the lower-level implementation details. This approach helps ensure that the system is developed to meet the needs and expectations of its users. It also encourages component testability and decoupling by encouraging the creation of small, focused tests and modular code.
By practising outside-in TDD, developers can gain confidence in their code and ensure the system behaves as expected. It also helps uncover design flaws early in the development process and encourages the creation of loosely coupled and easily maintainable code.
Overall, Outside-In TDD is a methodology that combines testing and development, focusing on providing value to users and driving the development process from an outside perspective.
Acceptance Driven TDD
In test-driven development, system tests are continuously developed or at least specified before the system. In test-driven development, system development is no longer to meet written requirements but to pass specified system tests.
Acceptance Test-Driven Development (ATDD), while related to Test-Driven Development, differs in approach from Test-Driven Development. Acceptance test-driven development is a communication tool between the customer or user, the developer and the tester to ensure the requirements are well described. Acceptance test-driven development does not require automation of test cases, although it would be useful for regression testing. The acceptance tests for test-driven development must also be legible for non-developers. In many cases, the tests of test-driven development can be derived from the tests of acceptance test-driven development. For this approach to be used for system security, the boundary conditions must go beyond the normal technical aspects, which are only considered in some cases.
What test coverage should I use?
It is important for the effect on security that tests run automatically. These then lead to test coverage that can be measured and compared to previous runs. The question arises as to which test coverage will be most suitable. It has been shown that there are significant differences in the strength of the respective approaches. I am a strong proponent of mutation test coverage, as it is one of the most effective test coverages. If you want to know more here, I refer to my video on “Mutation Testing in German” or “Mutation Testing in English” on my YouTube channel. In any case, it is important that the better the test coverage, the greater the effect that can be achieved.
How exactly is the security of the application supported?
1. Identify vulnerabilities early: Writing tests before implementing features help identify potential security gaps early. By considering security concerns during the test design phase, developers can anticipate possible attack vectors and design their code with security guidelines in mind. This proactive approach makes it possible to detect security problems before they become deeply embedded in the code base.
2. Encouraging Safe Coding Practices: TDD encourages writing modular, well-structured, and testable code. This enables developers to adopt secure coding practices such as input validation, proper error handling, and secure data storage. By writing tests that simulate different security scenarios, developers are more likely to consider edge cases and verify that their code behaves securely.
3. Regression Testing for Security: TDD relies on running automated tests regularly to maintain existing functionality as new code is added. This approach helps prevent a decline in security-related issues. If a security-related test case exists and initially passes, subsequent changes to it can be tested to ensure that vulnerabilities were not accidentally introduced.
4. Building a Security-Aware Culture: TDD’s incorporation of security testing as an integral part of the development process helps foster a security-aware culture within the development team. By performing security-focused testing alongside functional testing, developers are more likely to prioritize security and see it as a fundamental aspect of their job. This mindset encourages a proactive attitude towards security rather than just seeing it as a side issue.
5. Facilitate Collaboration and Code Review: TDD encourages collaboration and code review between team members. When developers first write tests, they can discuss security concerns and get feedback from peers. This collaborative approach increases overall security awareness within the team and enables early detection and resolution of security issues.
Although TDD is not a security panacea, it offers a systematic and disciplined development approach that indirectly strengthens software security posture. By promoting security-centric thinking, promoting secure coding practices, and facilitating early detection of vulnerabilities, TDD can help build more secure and resilient applications. However, it is important to note that additional security measures such as threat modelling, code analysis, and penetration testing should also be included to meet security requirements fully.
A few more practical approaches
What are Compliance Issues, and how to remove them?
The compliance issues are elements that run under a license and may not be used in this way in the project. This can happen in all technology areas within a project. Libraries or frameworks used do not usually change their license over the years, but it occasionally happens. All dependencies of the elements used must also be checked. And that’s up to the last link.
One must replace the affected element with a semantic equivalent available under an appropriate license to fix such a violation.
What are Vulnerabilities, and how to remove them?
Vulnerabilities are unintentional errors in the source code that allow the system to be manipulated to its advantage. These errors can occur throughout the tech stack. It is important to recognize all vulnerabilities in all technologies used to get an overview of which vulnerabilities can be exploited in which combination for an attack on this system. Individual considerations need to be clarified at this point. This holistic view enables the recognition of the different attack vectors and the identification of weak points that cannot be exploited in this system. This allows you to use your efforts in a targeted manner to secure the system.
Why do we need efficient dependency management?
What tools are available to developers to address compliance issues and vulnerabilities? The answer is obvious. This is where very effective dependency management combined with robust test coverage comes in handy. Because, in all cases, dependencies have to be exchanged. Even though it’s a surrogate for compliance issues, fixing vulnerabilities is the same dependency in a different version. This can be a higher or lower version. Overall, it is about the clever combination of the various elements. Strong test coverage facilitates the subsequent test to determine whether the functionality is still available.
In summary, strong test coverage is beneficial when modifying and exchanging external elements as well as your own source code. Here you can rely on the results of the CI environment without having to add manual verifications and thus implement a fast and lean release process with as little time loss as possible.
Quality and safety support each other.