Imagine the following scenario: You have that huge program with a couple of hundred modules.
And then you have to move something in the middle. Maybe it’s taking a class or a function and dividing it into two, changing the shape of some parameters, making some change in a certain structure, something like that.
How do you know that your change went right and didn’t break anything that was already working? Simple, it’s "just" testing. And how do you test? Just open up your program, start putting in data, and keep an eye out for him to behave as expected. It is also relevant to test for error situations when the system is fed incorrect data or is running in a problem environment to see if everything goes as expected. If you see that something broke unexpectedly, you have what is called regression.
But testing is not something easy, especially when the system is big. Also, doing all this manually is:
Very annoying, running manually always the same tests to make sure that nothing broke unexpectedly.
Subject to mistakes, because often you forget to test some situation or some special case, which is precisely what you broke.
Time consuming, because to test all the functionalities of the system, it takes a long time, mainly for a large system with hundreds of functionalities.
Well, how can we tackle these problems? All of these manual test problems are centered on the fact that the one person is testing. The work is repetitive, tiring and time consuming. What if we made a program that ran the test automatically?
So that’s where automated testing comes in! You write a program that will open the system features, log in with data and verify that the results match what was expected. Your automated test will also simulate error situations to see if your program reacts to it as expected. So:
Running the automated test is no longer boring. Just click a button, let it work on its own and at the end it says whether it all worked out right or not.
It is no longer subject to errors, as your automated test will always do the test the same way. So you don’t risk forgetting to test something.
The test is no longer time-consuming (at least not as long as a human), as he doesn’t have to wait for a flesh-and-blood being to type, drag the mouse or remember what the next thing to do in the test.
Ok, but automated testing and unit testing are slightly different things. What exactly is a unit test?
A well-designed code is modular and because of this it makes sense that you can test the modules independently of each other. In addition, a well-made modularization reduces the risks of you having changes in functionality cascading side effects where it is not expected. And that is what unit testing is: Unit testing consists of an automated test whose purpose is to run tests only on a small isolated unit of your system.
For example, let’s assume you used this javascript function:
function valida_data(dia, mes, ano) {
return dia <= 31 && mes <= 12;
}
You may notice that there is something wrong with it. But let’s assume that it has gone unnoticed and in the middle of your huge system it is there. Then suddenly someone says that in the issue part of the invoice appeared a date of April 31st. You will then begin to do a lot of debug in the notes, in the database, to spend hours and hours on end trying to find the bug, until you finally arrive in this function. And then you fix for it:
function valida_data(dia, mes, ano) {
var dias = [0, 31, 28, 31, 30, 31, 30, 31, 30, 30, 31, 30, 31];
return dia <= dias[mes] && mes <= 12;
}
And that’s it, April 31st is correctly rejected.
A few months later, someone complains that there was an error in the XML integration of the database. You will search in XML and have a lot of crazy error messages in the log, keep tracking and debugging the whole night and discovers that it was something that went wrong on August 31st. After several hours of debug you return to function valida_data
and finally realizes that for August is 30 and not 31.
Okay, you’ve already spent two nights cracking your head because of this function. Is there anything else wrong with it? Well, let’s create some tests to know:
function testa_valida_data() {
if (!valida_data(1, 1, 2015)) throw new Error("Não aceitou 1 de janeiro");
if (!valida_data(31, 1, 2015)) throw new Error("Não aceitou 31 de janeiro");
if (valida_data(32, 1, 2015)) throw new Error("Aceitou 32 de janeiro");
if (valida_data(30, 2, 2016)) throw new Error("Aceitou 30 de fevereiro");
if (!valida_data(29, 2, 2016)) throw new Error("Não aceitou 29 de fevereiro");
if (valida_data(29, 8, 2015)) throw new Error("Não aceitou 29 de agosto");
if (valida_data(31, 3, 2015)) throw new Error("Não aceitou 31 de março");
if (!valida_data(31, 4, 2015)) throw new Error("Aceitou 31 de abril");
// ... Outros testes
}
If you run the function testa_valida_data()
, she will throw an error! This then gives you the certainty that there is something wrong in the function valida_data()
. Then you go into the function and move it. To know if what you did is right, just rotate the testa_valida_data()
again. When the function testa_valida_data()
not throw a mistake, then maybe the function valida_data
be sure.
Why maybe? Because if the unit test passes, there is no guarantee that there is no more forgotten detail. On the other hand if it fails, then you are sure that there is something wrong and the test will already tell you what is wrong and where the error came from, saving a lot of time debug. Also, although the unit test can’t guarantee you that your code is correct, the more and the better the tests, the less likely it is that something that is wrong may have been forgotten.
What about Qunit? Qunit is a framework that gives you flexibility and manageability in testing. After all, if we keep things the way they are in testa_valida_data()
and there are several mistakes, it will only show the first and stop. Also, if there are multiple functions or modules to test, we will still have to call the test functions manually. With Qunit this is mitigated. Take this example:
function valida_data(dia, mes, ano) {
var dias = [0, 31, 28, 31, 30, 31, 30, 31, 30, 30, 31, 30, 31];
return dia <= dias[mes] && mes <= 12;
}
QUnit.test("Testa dias normais", function(assert) {
assert.ok(valida_data( 1, 1, 2015), "Aceitar 1 de janeiro");
assert.ok(valida_data( 7, 8, 2015), "Aceitar 7 de agosto");
assert.ok(valida_data(13, 12, 2015), "Aceitar 13 de dezembro");
});
QUnit.test("Testa últimos dias", function(assert) {
assert.ok(valida_data(31, 1, 2015), "Aceitar 31 de janeiro");
assert.ok(valida_data(28, 2, 2015), "Aceitar 28 de fevereiro");
assert.ok(valida_data(31, 3, 2015), "Aceitar 31 de março");
assert.ok(valida_data(30, 4, 2015), "Aceitar 30 de abril");
assert.ok(valida_data(31, 5, 2015), "Aceitar 31 de maio");
assert.ok(valida_data(30, 6, 2015), "Aceitar 30 de junho");
assert.ok(valida_data(31, 7, 2015), "Aceitar 31 de julho");
assert.ok(valida_data(31, 8, 2015), "Aceitar 31 de agosto");
assert.ok(valida_data(30, 9, 2015), "Aceitar 30 de setembro");
assert.ok(valida_data(31, 10, 2015), "Aceitar 31 de outubro");
assert.ok(valida_data(30, 11, 2015), "Aceitar 30 de novembro");
assert.ok(valida_data(31, 12, 2015), "Aceitar 31 de dezembro");
});
QUnit.test("Testa além dos últimos dias", function(assert) {
assert.notOk(valida_data(32, 1, 2015), "Rejeitar 32 de janeiro");
assert.notOk(valida_data(30, 2, 2015), "Rejeitar 30 de fevereiro");
assert.notOk(valida_data(32, 3, 2015), "Rejeitar 32 de março");
assert.notOk(valida_data(31, 4, 2015), "Rejeitar 31 de abril");
assert.notOk(valida_data(32, 5, 2015), "Rejeitar 32 de maio");
assert.notOk(valida_data(31, 6, 2015), "Rejeitar 31 de junho");
assert.notOk(valida_data(32, 7, 2015), "Rejeitar 32 de julho");
assert.notOk(valida_data(32, 8, 2015), "Rejeitar 32 de agosto");
assert.notOk(valida_data(31, 9, 2015), "Rejeitar 31 de setembro");
assert.notOk(valida_data(32, 10, 2015), "Rejeitar 32 de outubro");
assert.notOk(valida_data(31, 11, 2015), "Rejeitar 31 de novembro");
assert.notOk(valida_data(32, 12, 2015), "Rejeitar 32 de dezembro");
});
QUnit.test("Testa zeros e negativos", function(assert) {
assert.notOk(valida_data(-1, 1, 2015), "Rejeitar dia negativo");
assert.notOk(valida_data( 0, 1, 2015), "Rejeitar dia zero");
assert.notOk(valida_data( 1, 0, 2015), "Rejeitar mês zero");
assert.notOk(valida_data( 1, -1, 2015), "Rejeitar mês negativo");
assert.notOk(valida_data( 0, 0, 2015), "Rejeitar mês e dia zero");
assert.notOk(valida_data( 0, -1, 2015), "Rejeitar dia zero de mês negativo");
assert.notOk(valida_data(-1, 0, 2015), "Rejeitar dia negativo e mês zero");
assert.notOk(valida_data(-1, -1, 2015), "Rejeitar dia e mês negativo");
});
QUnit.test("Testa bissextos", function(assert) {
assert.notOk(valida_data(29, 2, 2015), "Rejeitar 29 de fevereiro não-bissexto");
assert.ok (valida_data(29, 2, 2016), "Aceitar 29 de fevereiro bissexto");
assert.ok (valida_data(29, 2, 2000), "Aceitar 29 de fevereiro bissexto");
assert.notOk(valida_data(29, 2, 1999), "Rejeitar 29 de fevereiro não-bissexto");
assert.notOk(valida_data(29, 2, 1998), "Rejeitar 29 de fevereiro não-bissexto");
assert.notOk(valida_data(29, 2, 1997), "Rejeitar 29 de fevereiro não-bissexto");
assert.ok (valida_data(29, 2, 1996), "Aceitar 29 de fevereiro bissexto");
assert.notOk(valida_data(29, 2, 1900), "Rejeitar 29 de fevereiro não-bissexto");
assert.notOk(valida_data(29, 2, 1800), "Rejeitar 29 de fevereiro não-bissexto");
assert.notOk(valida_data(29, 2, 1700), "Rejeitar 29 de fevereiro não-bissexto");
assert.ok (valida_data(29, 2, 1600), "Aceitar 29 de fevereiro bissexto");
assert.notOk(valida_data(29, 2, 2100), "Rejeitar 29 de fevereiro não-bissexto");
});
QUnit.test("Testa mês depois de dezembro", function(assert) {
assert.notOk(valida_data(10, 13, 2015), "Rejeitar dia do mês 13.");
assert.notOk(valida_data(10, 14, 2015), "Rejeitar dia do mês 14.");
assert.notOk(valida_data( 0, 13, 2015), "Rejeitar dia zero do mês 13.");
assert.notOk(valida_data( 0, 14, 2015), "Rejeitar dia zero do mês 14.");
assert.notOk(valida_data(-1, 13, 2015), "Rejeitar dia negativo do mês 13.");
assert.notOk(valida_data(-1, 14, 2015), "Rejeitar dia negativo do mês 14.");
assert.notOk(valida_data(32, 13, 2015), "Rejeitar dia 32 do mês 13.");
assert.notOk(valida_data(32, 14, 2015), "Rejeitar dia 32 do mês 14.");
});
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-2.6.0.css">
<script src="https://code.jquery.com/qunit/qunit-2.6.0.js"></script>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
If you run the above tests, you will see that many of them fail. The function valida_data()
which I have provided is purposely flawed, and just click the button "Execute" up there to see that.
If you copy and paste this snippet somewhere else and change the function valida_data()
, you can check if your changes are correct just by clicking a button. With this, you can keep changing it and quickly know if what you did is right or not, until all the tests pass. I leave this task as an (easy) exercise for the reader. :)
Let’s assume you changed the code of the function until all the tests pass. If some time after that (possibly years later), you have to change the function for some reason (e.g., found a better performing way), then how do you know if you haven’t messed up with anything or introduced a bug? Just click on the button "Execute" and see the result. If you have entered an error, it is very likely that it will appear there. If the test did not show any errors, your changes are likely to be valid and reliable. And so you can have a quick, thorough, efficient and detailed test with just one click.
Also, if years later someone finds a bug that was not covered in any test, all you need to do is add a test for the bug, and change the code to fix it. Your test ends up serving as a guard against regressions. Because if the bug comes back, the test will break and report this fact. And of course, if a new bug is introduced, the existing tests are highly likely to report it.
On the other hand, to be frank, there’s a downside: You need to write the test code, it doesn’t fall out of the sky! And writing test code takes some time, but you’ll save this time later with debugging that you nay you’ll need to do. In addition, having a wide and good coverage of tests is difficult, but even so the higher the coverage of the tests, the lower the probability that some change in the code breaks something unexpectedly.
And of course, it’s possible (and common good) that the tests are wrong, which results in tests that accept wrong code, or reject right code, or simply don’t properly test what they propose to test. It is also common that there are fragile tests, which break because of innocuous and harmless changes, or loose tests, which do not break even when harmful and dangerous changes occur. These problems with testing can have several causes, such as: poor quality of tested code; poor quality of test code; low test coverage over tested code; and confusing and poorly defined design requirements (How will you properly test something that even you don’t know exactly what to do?). Anyway, writing good tests also requires a certain experience, but to have experience, there is only one way: to put your hand in the dough and practice.
Link to get Qunit: https://qunitjs.com/