Article originally published on habr.com (in Russian) Author: Vitaliy Ivashchenko
There comes a moment when every developer starts thinking about writing tests for his creation.
No — check that — every good developer.
When you’re still a junior developer and you don’t have as many responsibilities. You’re not responsible for the product that you create and, as a result, you don’t have the motivation to spend extra time on rechecking the code you wrote.
- “It’s alright, this mistake won’t be reproduced.”
- “It seems to be working.”
- “Well, at least it does what I need it to.”
If you want to grow beyond a programming noob, you’ll have to change your attitude towards testing.
As you progress up the career ladder, you get to take part in more and more cool projects and work with big clients. If you’re lucky, all your projects will for big-name companies that are respectful and pay well and aren’t hypersensitive to problems that emerge.
Let’s look at a simple case – very simple – but the important thing is the idea behind it.
The case: A form handler created by a programmer who didn’t want to put in any effort.
The task was to write a form handler to accept customer requests for the purchase of bricks. The client is a big company, engaged in large deliveries of bricks for wholesale. Let’s say minimum $7,000 per order (so you understand the gravity of the situation). Competition is high and customers can quickly switch to a different supplier if they don’t get a response within 24 hours.
”If you want to grow beyond a programming noob, you’ll have to change your attitude towards testing.”
Our programmer was given the following requirements: the form has to save customer’s data including — first name, last name, phone number, company name, order volume and optional description. After some thought, a super-simple form was created with standard fields.
Data from the form is sent by AJAX request, without reloading the page. Next, the programmer has to set up the form handler to solve a rather trivial task — add an entry to the existing “Orders” table (in the database) and send an email to the sales department with the information about the new customer.
The form works, the data is successfully saved and the client is satisfied.
But suddenly an angry call comes in from the client
Client: “We received an order for a high-end company. They want to buy all of our bricks, but their phone number didn’t show up in the database, so how do I contact them now?! Tomorrow they’ll find another supplier! How could you let this happen?! It’s all your fault …”
The client is furious and we’ve lost his trust and respect. You could say it’s a common theme for junior developers — a lack of validation and testing of incoming data. We solve the validation problem by adding rules:
In the future, customers visiting the site will be required to enter certain fields for further processing. And suddenly, a thought comes across the developer’s mind to test the code before problems arise. For example, to test the last name field, we do the following (to simplify the basic example, CSRF protection is disabled):
We know that if this field is empty, the site will return a 400-status error code. This type of testing should be done for each test case (or each field that needs to be validate).
But is there any other way of developing software other than, “I did it, now I’ll see if it works?”
How can we prevent problems from arising in the first place?
First, we write the code, then we run into execution problems. After correcting the issues, we remember that we need to test. This approach can become a thorn in our side (and our client’s side), possibly leading to lost multi-million dollar contracts (albeit theoretically, but here’s to wishful thinking).
And then I wondered — what if we start creating the application logic from the “end?” Instead of first writing code, we can create requirements for the “contractor” and then meet these requirements. Let’s try.
“And then I wondered — what if we start creating the application logic from the “end?” Instead of first writing code, we can create requirements… and then meet these requirements. “
Enter test-driven development
Let’s leave the task the same, and only change the approach. We need to write a form handler with the fields first and last name, phone number, company name, order volume and optional order description. Successful execution should return a 200 status, and an entry should be added in the “Orders” table with a message — “OK”. All other test cases should return a 400 status with a corresponding error message depending on the field(s).
First of all, we need to write a method for testing form validation.
Next, we create the necessary route and controller method (while the field is empty). If we run the check now, we will get an error. But checking for valid data is not all we need. So we also have to validate the required form fields — first and last name, phone number, company name, order volume and optional order description.
The form handler will only be required to check the input for first and last name, phone number, company name, order volume. Since we have removed all required fields from the query, this should return an error for each of them. If at least one of them is not there, this is an implementation problem. If you want, you can add a check to the message, as was done previously (check for “OK”).
We check for the minimum length of the required fields (similarly, the maximum length and invalid characters in these fields will be checked).
We’ve written all of the checks and now we can run it and make sure it works.
Fine-tuning the tests
Perfect. Our application crashed in 5 out of 5 tests. Our next goal is to go through test methods that set invalid values in the fields and form validation rules for the input data. The logic is like this: the first/last name field cannot be empty; minimum length — 3 and maximum — 120; it’s a string with a set of characters allowed in the name (letters, hyphen, indentation). The result of such logic in all fields:
In response to a failed test, a list of errors that correspond to each “problem” field is added. This will help us validate specific fields (assertJsonStructure in the test file). Next, we add the method to a valid check and get the final version:
Finally, we check how our script fulfills the testing (Just to remind you, there were 5 out of 5 failed tests previously).
As you can see, the tests are successful and only one entry was made to the database.
What are the conclusions? A new perspective on test-driven development
A test-first approach is a better option than just writing functional code.
Getting only what you need from a method is similar to the efficiency of a strong fighting force. The code does exactly what you require of it, nothing more, nothing less.
However, this approach also has a downside. The fact is that writing additional code (for testing purposes) also takes up development time. For me, the choice is unambiguous — a good programmer should write tests. Test-driven development helps to write optimized, reliable code.
My next step is to try to this approach in something less trivial.
Cover Image Credit: Gabe Kronisсh, Flickr