Everything you need to know about Test Driven Development
Hello everyone, my name’s Edmond Aouad and I’m a co-founder and Chief Product Officer here at Ponicode.
Over the past two years, I’ve had the incredible opportunity to interview, talk to, and exchange ideas with hundreds of clients, developers and companies with very large tech teams. Like I was, you might be surprised to discover just how diverse unit testing practices are today. For example, did you know that developers don’t all test at the same time in the development process? Below are three further examples, among many many more:
Unit testing months/years after the code is put in production: Changing legacy code in production is very risky, so unit testing is the best way to protect it before making any change. We often see this need for unit testing in Java; this is why Java is at the core of Ponicode’s roadmap. An alternative is to rely on feature flags or canary releases to run tests in production. Testing in production does not mean deploying untested code nor is a replacement of unit testing, but is a safe way to progressively release a new feature.
Unit testing just before committing a new feature: When the core of a feature is coded, it should be protected from regressions. In the majority of cases, this is when developers start unit testing: when the code for the feature is developed. Then code quality checks (code coverage, mutation score, …) are carried out, to verify that the code is “well protected”. It goes without saying that a relevant scoring is key to assessing the tests.
Unit testing before beginning to code a feature: Some developers use a “test first” method, where they write their unit tests before writing the code for their features. Here, the test dictates what the code should do instead of the other way around. Once the tests are completed, the developer starts coding and runs their tests until all of the tests pass. Once all the tests have passed, the code is considered complete. This is called Test Driven Development (TDD).
While TDD makes up roughly only 3% of the market, the Ponicode vision is to be the platform to help all developers by solving code quality issues, and our beloved TDD developers are very dear to us, so I decided to write an article about Test Driven Development unit testing. Besides, we have many TDD developers in our community who use Ponicode to enhance their code quality and I want to share how they do it with the rest of the community!
On the agenda today:
- Why develop using TDD?
- The limits of TDD
- Use cases where Ponicode can boost TDD
Why develop using TDD?
A goal-oriented approach
Sure, in “TDD”, the first letter stands for “Test”. However, when you create your test, you are writing the expected outcome of your code. You are essentially writing specs and the code is just a means to reach these specs.
The goal is that your tests pass.
The test architecture is the action plan.
The means is your code.
Yes, it is less “attractive” or “fun” than jumping straight in and starting to code, because it forces you to think before you act. But in the end, the cleanest way to reach a goal is to formalise it, write a plan and then implement it. This applies to code efficiency and TDD enables this discipline.
Good architecture is very often overlooked when coding, and this causes many unwanted side effects overtime:
- Slow releases
- Poor quality VS innovation ratio
- Technical debt
- Developer frustration
- Spaghetti code base
These are the very same problems that come with untested code bases. TDD forces you to think about your goals, and consequently, you have to think about the correct architecture to optimise your tests. Your brain will be in “design mode” and naturally prompt you to build strong architecture that enables clean tests that don’t involve lots of mock objects and other unattractive syntax.
Of course, one consequence of TDD is that all of your code is tested. Thus, you have 100% code coverage and your code is protected from any potential regressions.
Finding bugs at the earliest stage
In 99% of cases, unit tests show bugs in the code just after it was written (when not doing TDD). By testing first, the bugs are found at the very beginning of the development process. The later a bug is found, the harder, more annoying and more costly it is to fix. To give you an idea, a bug costs from 100 to 1000 times more to fix when it is found in production as opposed to during the development process. TDD minimizes this cost.
Other benefits that I won’t dig into:
- Less regression testing
The limits of TDD
On paper, and also in many cases in practice, TDD seems to be one of the best available approaches to developing software. In particular, it enables you to shift left and reduce all costs associated with code quality.
However, the majority of the market does not match the requirements for ideal TDD conditions in most cases:
Legacy code: TDD does not apply to legacy code because the code was already written.
High initial cost: TDD needs a “no rush” environment. The pressure of quick time-to-market contradicts this condition. It is somewhat of a luxury and pretty rare to have the time available to think thoroughly about the test and architecture before beginning to code. While not ideal, this is the reality of TDD => Here Ponicode can help to reduce by 5 to 10 times the cost of writing the tests before starting to code and enable TDD even in high pressure environments.
Extensive code robustness: TDD certainly helps reduce bugs early in the development process, but unfortunately, there are always bugs that are missed by the developer => Ponicode has been trained to be able to spot bugs in existing code extensively and help developers find bugs they did not consider.
Extensive test cases: In any top down approach, some axes can be forgotten. This is the same for test cases, some values for happy paths can be forgotten by the developer => Ponicode has been trained to give mutants of tests to suggest exhaustive test cases that look like existing test cases with relevant variations.
Extensive edge cases: Edge cases are very often forgotten by developers, even in TDD => Ponicode will suggest edge cases that the developer did not consider.
Where can Ponicode boost TDD ?
No tedious and time consuming parts of testing: Thanks to the Ponicode interface, you can skip all the tasks associated with the body of the test.
What Ponicode does for you:
- Writes the correct imports
- Writes the skeleton of your tests
- Creates the instantiation of the methods
- Runs your tests so you can see in real time whether they pass or not
You can focus on the important part of your tests:
- Writing relevant inputs
- Writing relevant mocking
- Writing your edge cases
Let’s take the example of the function get_and_validate_department from the repo timeoff-management-application
Here’s how I would test it with a TDD approach using Ponicode:
- Write the name of the function with the parameters
- Open the interface by clicking on “Ponicode Unit Test”
- Manually fill my test cases
- My test case is written ! All I have to do now is code my function
- Did not have to write the imports
- Did not have to write the skeleton of the test
- I use the Ponicode runner to see in real time if my function passes the tests
I can see the real time code coverage and whether my function is passing the tests or not
When everything is green and 100%, I am done !
Early bug detection and code robustness:
While writing my tests, I may have forgotten some potential errors
What Ponicode does for you:
- Suggests edge cases and variants of value formats to increase code robustness
- Early bug detection: spot bugs before going to production
Here, Ponicode suggested edge cases that would make the application crash
Mutants of existing test cases
While writing my tests, I’ve considered some “perfect” scenarios I am expecting from the user. Ponicode was trained on millions of lines of real life values that show what the user of an application could enter vs what is expected in a perfect scenario. We call these mutant values
What Ponicode does for you:
- Suggests real life alternative test cases representing natural values that the user of the application will likely enter
- Realistic tests
Here we have two kinds of mutants:
- Diversity: Ponicode suggested “fr”, for example, as an additional language to test that was not tested in my original tests
- Error in typing: Ponicode suggested “a-r”, for example, a language with an error from the user typing that is very likely to happen in production
With all of this added value, Ponicode makes it much easier to apply Test Driven Development in any business environment. We know that developers care about code quality and would love to spend more time on such topics in their daily lives.
That’s it for today! I hope you enjoyed this article and that many of you TDD developers will go on spreading best practices for code quality and good architecture. If you are in a working environment with unfavourable conditions, I hope Ponicode will help make better quality code possible throughout your company! 🚀🦄🌈