How to engineer reliable software
Yet another article about TDD.
Ten artykuł jest również dostępny w języku polskim na LinkedIn.
This article is made from the lightning talk that I have presented to my work colleagues.
Why should you write tests?
Firstly: tests allow us to write code that is cleaner because the engineer is focused on goals that were presented by “business” (product owner/client).
Another advantage is the higher quality of the code. The code is tested so we can refactor it, introducing improved architecture — without worrying that other important parts of the system will be broken by our changes. We are going to see errors before deploying an application on production and being woke up by calls from users.
If we have the tests — we do not need to test our work manually. Most of the problems will occur while running test commands. Of course, we should “point and click” in our software — just in case — on staging or pre-prod. However, we do not need to use tools like Postman as often. When we spent less time on manual testing we have more time to write “The Meat” — software that actually is doing something valuable to the business and therefore we are going to deliver more at the same time.
When some tests are written and we do not need to manually check everything every time — with high probability we will try to have as high code coverage as possible — because we (as humans) are lazy and we love to make things simpler and faster. With higher coverage, we also have better documentation. Yes — tests can act as a book for new developers as well as for us after some time when we don’t remember what that particular part of code was doing.
Automated tests will outcome in less fails on production because most of “bad code” is going to show up before deployment as the red screen will appear saying “test failed”.
Writing tests in the long-term is going to allow write more code than in the non-TDD approach. We need more time to set-up tests and build configurations; however, later, we do not spend so much time on regression tests — testing again-and-again the same code.
How exactly writing tests is going to help me engineer better software?
You do not need to spend as much time on debugging and testing as the tests are going to do it for you due to automation and simplicity of running them.
Your focus is on the goal rather than thinking about architecture. Not that architecture is not important. It is! Just use patterns and “big architecture” when it’s needed; not on simple, less important methods.
With tests, the specification is created. The tests show what business aspects your application is capable to handle and what is not.
When you are writing unit tests you are going to see that it’s better to inject objects rather than creating them inside classes. And that not coupling code so tightly is better because you can test more cases and do it simpler.
Refactoring is no longer going to scare your team. You have tests that cover multiple cases that would never be tested otherwise. You can introduce better architecture, change how things work without worrying that other part of the system is going to fail for your clients.
Traditional, non-TDD approach.
In non-TDD approach typically our workflow would look like this:
We are given business requirements. With the team, we are trying to sketch how the solution will look like and then we are dividing the problem into subtasks.
Engineers are taking tasks, start to code “the meat” and in the meantime or after some tests are written. However, in most cases, the number of tests approaches zero in this “model”. All of the testing is done by hands rather than automatically.
When somebody — or we — want to make small changes in the code — we need to repeat the test process manually again.
There is a better way!
Test-driven approach to engineering software.
In the TDD you still have the design phase where you are going to discuss the problem and design the architecture. But now there is a new aspect: you should also take on the board test scenarios that are important to your business.
When you have a task you should start not from coding business logic but rather start with writing code that will test your future work. And yes — when you run new tests for the first time all of them should fail and be red.
This way of working gives you another advantage: you do not need an internet connection(very often) nor the container on the cloud. Just your device with simple Node.JS and packages installed or other that you are using on your environment. No database, cache nor third party services. One command and most times you know whenever what you have written will work on production.
Red » Green » Refactor
Start with the test — it should fail at the beginning. Then write just enough code to pass the test. When the message is “test passed” with a green background start refactoring code and play with architecture design.
Of course in real life, you will rather write a few tests or more code than required by test — it’s all about the balance — the outcome should be that you do not need to do manual verification whenever everything is working properly 🙂
Things to avoid while writing unit tests.
Unit tests must be fast — you cannot use anything slow. Therefore you shouldn’t use a database, call 3-rd party service neither make requests to API that is owned by you. If you need to — mock such service.
You shouldn’t use filesystem nor bash nor Bluetooth Xbox controller via /dev/btjoy.
Your tests must be independent of the platform that they are running on. You shouldn’t use parallel parts, that could affect the way other test works.
Treat tests as independent entities. As tests must be fast — you cannot purposefully slow them down using setTimeouts or sleeps — in Jest you can fake them so they are instant.
And if you shouldn’t touch files you also shouldn’t mess with environmental variables nor configurations.
Tests should be F.I.R.S.T
That famous acronym means that the tests must be as fast as possible — to not discourage developers from running them. This must take a few seconds and not minutes nor hours 🙂
To make tests fast they must run at the same time(parallel) so one test must be independent of another. Every test should have own dataset. Tests should be written in such a way that they can be randomly run with no order — therefore for example date set by one test cannot be used by the next test.
Tests should be self-validating so we do not need to manually check results. We should get just a red(fail) or green(pass) outcome.
And last but not least — we are not writing tests just to cover 100% of the code. The tests should cover important business scenarios and edge-cases.
The tools are used in projects that I am working on. Below you have them briefly described.
Jest is a testing framework that is fast, simple to use and has a ton of options to check whenever the data is correctly processed. It also has great documentation.
Jest has tons of assertions that you can use. You can assert value, search in strings, arrays, objects or go deeper and spy on calls to methods. Just to name a few.
Another advantage of Jest is a code coverage report. You can check whenever you have tested a particular file, branch(if/else statement) or line. Remember that you shouldn’t just try to achieve as high coverage as possible — most important part is to cover crucial business scenarios 🙂
Bamboo is a tool that allows us to connect each part of the deployment process. You can configure it in such a way that on every commit Bamboo will run unit tests, code linters and run the SonarQube analysis. We are using a connection between Bamboo and BitBucket (GIT repository). We are not allowing deploy nor merge to master when tests are not passing.
There was TDD Live Coding Session with examples that are available on https://github.com/niezgoda/LT
Live Coding Video
I have created a video from coding simple Fizz Buzz in TDD-style.
Sources & more