Test-Driven Development with Python: Why And How Should You Do it?
Apr 18, 20228 min read
Senior full stack developer and CTO at Ideamotive.
The contemporary environment of software development is diverse enough to have different approaches to coding. The oldest and the most conventional one is an architecture-driven method. This is when an application is completed with all desired features, and tests are run afterward. A “test-last” concept lies in the background of such a development method, therefore.
The approach originates in the days when agile development has not been widely adopted yet. The biggest disadvantage of the method is that the whole team has to check the entire code base for hours or even days to fix just a single line of code when one of the tests fails.
A decent alternative to the architecture-driven approach is test-driven development (TDD). The method appeared in 1999 under a “test-first” concept that was an element of extreme programming (XP). Later on, TDD became a standalone methodology, however.
Test-driven development offers a sequence of actions that is reverse to architecture-driven programming: tests for every code unit are created prior to the very code appearing. It may sound irrational and time-consuming at first glance. The final effect, however, is the opposite: only clean code is delivered to production with no redundant time spent on bug fixing.
Is test-driven development relevant to every project? Can the approach be applied to programming in Python? Where to find Python developers who possess hands-on TDD? The present post aims at answering the questions as well as delivering some fundamentals about test-driven development.
What is test-driven development?
TDD is a software development technique based on very short recurrent cycles consisting of the following three stages:
A test for a particular function is written first;
A code unit that should pass the test appears second;
Refactoring of clean code occurs when the test is successfully passed with the code.
One of the critical aspects of test-driven development is that programmers have to create tests on their own without assigning testers. The teams staying far from TDD may argue that this is not a programmer’s business to write tests. It is valid for the traditional test-last approach when tests are run post factum after a particular development stage is finished. As a result, the following typical drawbacks of the test-last method take place:
Compromised quality of the final product;
Testers appear to be whip boys in terms of taking responsibility for the quality;
The cost of debugging is excessively high, especially in a post-deployment period;
Unsatisfied (or partially satisfied) customers refuse to return to the developers with new orders.
Test-driven developers think otherwise: there should be a test for every given module before a programmer writes code for it. Nobody but programmers have to create those module tests, therefore. Moreover, the first test always fails since no code is still available. Why so? It is worth grasping a typical TDD cycle in more detail.
How a test-driven development cycle works
TDD adherents divide a development cycle into three stages with several steps in each. The first “red” stage consists of the following:
Step 1: consider a code module to be written for a particular function;
Step 2: write a module test;
Step 3: run the test that will inevitably fail.
Some developers call step 2 “writing a failing test” not for nothing: there is no code capable of passing the test. “Any test deserves no trust unless it fails at least once,” they say. This is some sort of self-motivation when a failed test encourages developers to consider either the functional correctness of the future code or the test as such.
The second “green” stage includes the following:
Step 4: write a minimum possible code to pass the test. If the test goes green, the functionality of the code is correct. It is critical to make the code pass the test once and fail the test once. Making mistakes is easy: the always-passing tests, as well as always-failing ones, imply nothing special to create. But the test that can be as passed as failed definitely checks some kind of logic.
Step 5: Run the test again to make sure that the code’s functionality works well.
The last “refactor” stage of test-driven development implies writing clean code with no redundant elements and repeating steps from 1 to 5. The shorter the whole cycle, the better.
What benefits does test-driven development bring
Even though test-driven development can hardly be a universal approach applicable to every project, some apparent benefits of TDD cover a wide variety of development practices. The following ones can optimize the programming workflow, to name a few:
Developers better realize what the final result should be when they create tests to check the result before writing code;
Every code module is considered finished when the code passes the test. Passing tests and refactoring precede working on a subsequent code module;
Since a set of unit tests runs after each refactoring, feedback showing that every component is workable becomes continuous;
Module tests act as actual documentation that always corresponds to the data;
If a bug occurs, a developer creates a test to figure out what the bug is. A code that fixes the bug is written next to pass the test. Debugging time is reduced, therefore. Passing all the other tests makes sure that the entire functionality works properly;
Developers can make design decisions and do refactoring at any moment while running successful tests to guarantee that the system works well. Software becomes easily repairable as a result;
Every subsequent test additionally checks all previously discovered bugs, and the repeatability of similar bugs goes down;
Since a large part of tests runs during the development stage, a pre-deployment testing period becomes shorter.
Misconceptions regarding test-driven development
When a company hires Python programmers unfamiliar with test-driven development, some sort of opposition may arise due to biases and misconceptions that circulate among the developers. A lack of knowledge about the test-driven approach lies in the background of those misconceptions. Even though they may sound strongly logical, there are enough counter-arguments to debunk any wrong vision of test-driven development.
Misconception 1: TDD is all about testing and test automation.
Clarification: TDD is a programming methodology based on the “test-first” approach.
Misconception 2: TDD implies no designing.
Clarification: TDD includes critical analysis and design in accordance with technical requirements.
Misconception 3: TDD works at the level of a single code module.
Clarification: There are no constraints to using TDD at both the integration and system levels.
Misconception 4: TDD does not work in projects with traditional testing cycles.
Clarification: TDD is popular in extreme programming and other kinds of agile development. At the same time, nothing prevents TDD from being applied to the development stage of any traditional project in addition to its post-development testing stage.
Misconception 5: TDD is just a tool.
Clarification: TDD is a methodology when each new module test is to be added to the automation test suit. All the tests have to be run after any change in the existing code as well as after each refactoring. Hence, various means of test automation facilitate TDD, which is more than just a tool.
Misconception 6: TDD implies transferring the acceptance tests from testers to developers.
Clarification: TDD does not exchange testers for developers in acceptance tests since a set of specific development-centric tests such as unit and module tests are run by programmers at the development stage.
When test-driven development is worth practicing
Test-driven development can be accepted as a methodology by Python programmers when their project meets particular conditions. Some of them belong to external conditions, and some others do to internal ones. Thus, the choice depends on the following factors:
A team and/or a person who sets the project’s objectives. This is about the technical qualification of the customer.
Inner processes that go through the project’s workflows. TDD is hardly implementable if no continuous integration is practiced.
The following criteria seem to be helpful to recognize the cases when TDD is worth doing:
If there is a clear description of functions and modules, the challenge is to accelerate and optimize the software development life cycle. In such a case, developers do not need to invent the entire architecture on their own. They can write module tests while having a clear vision of the final system. Otherwise, they risk creating something different from what customers expect;
If there is a typical approach to function and modules, i.e. a unified logic for building architecture is available. In such a case, developers can arrange a testing environment with a certain number of templates. Otherwise, each particular test requires unique conditions that appear too time-consuming to let TDD bring any value;
If task setting goes in the style of “one code module implies one test”. When a module can be divided into several sub-modules, a single test won’t be enough.
If there is a high-level testing framework that supports integration tests and corresponds to the primary technology of the project. You better have such a tool in your programming language (Python in our case, but more on that later);
If no standalone testers are available in the team of programming engineers. When testers write tests, one of the primary advantages of TDD - a mental connection between a programmer and functionality - gets lost.
Test-driven development is not a panacea as we see. If TDD does not fit your project, the development process is getting slower, and programmers tend to come to the wrong generalizations such as:
TDD is applicable to product companies only;
We have no relevant technology stack to use in TDD;
The TDD methodology is not viable in general;
TDD fits backend development only;
Our software domain has nothing to do with TDD, etc.
To avoid such grim hesitations, it is worth assessing TDD from the perspective of your development activity. Test-driven development is hardly recommended if:
An order for software development is made by a customer with no sufficient technical background. Developers themselves have to consider all possible scenarios along with the entire architecture. Writing module tests risks becoming excessively time-consuming and inefficient in such a case. It is better to approve the functionality of the app with the customer first;
The business logic of the app is not pre-designed. Developers have no clear idea of how the functionality should work. Pre-coding tests will extend the project deadlines continuously in case of a poorly comprehensible functionality;
Sophisticated calculations or heavy loads are available in the project. The results of complex calculations are hardly predictable at the stage of writing unit tests.
To use or not to use TDD is an individual issue. A lot depends on the project, the development team leaders, and the corporate values you have. TDD provides no breakdown of patterns if the following basic principles are kept in mind:
Tests should cover the functionality, not the code;
Tests should be abstracted from implementations of code;
Tests should be run within their specific environment.
Why Python fits TDD as little else does
Whether Python is an appropriate technology for test-driven development seems to be an idle question, as many Python programming engineers believe. The very availability of relevant automation test suites and frameworks determines how well one or another technology can work in TDD. Python, in such a context, is one of a few programming languages that are rich enough with tools for modern testing practices.
Python is famous for its simple readability and ease of coding. Python automation engineers do not need to use compilers for converting code since the language is scripting. The “Zen of Python” automation guide claims that tests implemented via a test suite are easily readable and explainable.
Selenium automation framework, for example, allows using web browsers to create automated tests with elegant simplicity. Automation test scripts appear through the interaction of Selenium WebDrive with such browsers as Mozilla Firefox, Google Chrome, Internet Explorer, Safari, and Opera. The tool is compatible with Python and other scripting languages to create fast repeated tests in continuous integration pipelines.
Another Python-centric automation test framework is Robot. The framework allows developers to write code when a failing test is available only. A more explicit feature of test-driven development can hardly be found. Robot is written in Python and aimed at organizations that practice Agile development and TDD.
Many Python developers consider PyTest the best automation test framework ever. Unit tests, end-to-end tests, and integration tests all are easily implementable with the framework. Only a few Python programmers seem to prefer the by-default test suite PyUnit (unittest) instead of PyTest since the latter has a much wider scope of testing options.
In addition to the testing frameworks, some dedicated Python libraries suitable for TDD are also available. Hypothesis, for example, is the one that helps create more powerful and simpler-to-write unit tests capable of finding poor code where it can hardly be expected. The library is seamlessly compatible with any test suite.
As we see, Python is rich with testing toolkits that make test-driven development a highly appropriate approach to almost any software written in Python.
Answering the question “why you should use test-driven development” has several possible variants. First of all, TDD is about the uncompromised quality of code refined due to numerous refactoring. Debugging happens organically over the entire development life cycle.
Another aspect is time (and, therefore, money) saved due to a shorter pre-deployment acceptance testing. Much more elegant code appears in less time when progressive agile approaches, such as test-driven development, are practiced along with continuous integration/continuous delivery.
Few other technologies are more compatible with TDD than Python: various automated test tools are sufficiently available for the language. Test-driven development with Python is a formula of success for the customers who follow the zeitgeist.
Contact us today if you share our vision of the perfect “TDD + Python” combination capable of meeting the requirements of any software project. Top-notch development services and dedicated teams of professional Python programmers are always available for reasonable pricing.
Dawid is a full stack developer experienced in creating Ruby on Rails and React Native apps from naught to implementation. Technological superhero, delivering amazing solutions for our clients and helping them grow.