javascript – What are Unit Testing for and what are the advantages?

Question:

I saw some videos and articles about unit testing, learned about the QUnit framework and some test situations.

Still, I couldn't think of a practical use worth more than a common browser debug or exception handling, etc.

When and how should I actually use a unit testing framework?

Answer:

Imagine the following scenario: You have that huge program with a few hundred modules.

And then you have to move something in the middle. Maybe it's taking a class or a function and splitting it in two, changing the shape of some parameters, making some change to a certain structure, whatever.

How do you know your move worked and didn't break anything that was already working? Simple, it's "just" testing. And how do you test? Just open your program, start inputting data, and keep an eye out if it behaves as expected. It is also pertinent to test for error situations, when the system is fed with 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 a regression .

But testing is not easy, especially when the system is large. Also, doing all this manually is:

  • Very annoying, manually running the same tests over and over again to make sure nothing unexpectedly broke.

  • Error-prone, as you often forget to test a situation or a special case, which is exactly what you broke.

  • Time-consuming, as it takes time to test all the system's features, especially for a large system with hundreds of features.

Well, how can we attack these problems? All these problems with manual testing are centered on the fact that the person testing is a person. 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's features, enter data and check if 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 on a button, let it work by itself and at the end it says if everything worked out or not.

  • It is no longer error-prone, as your automated test will always test the same way. That way you don't run the risk of forgetting to test something.

  • The test doesn't take any longer (at least not as long as a human) as it doesn't have to wait around for a flesh-and-blood being to keep typing, dragging the mouse, or remembering what's the next thing to do in the test.

Okay, but automated testing and unit testing are slightly different things. What exactly is a unit test?

Well-designed code is modular and because of that it makes sense that you can test modules independently of each other. Also, well-done modularization reduces the risk of you having functionality changes cascading side effects where you don't expect them. And that's what unit testing is all about: Unit testing is automated testing whose purpose is to run tests on just a small isolated unit of your system.

For example, let's say you used this javascript function:

function valida_data(dia, mes, ano) {
    return dia <= 31 && mes <= 12;
}

Perhaps you notice that there is something wrong with her. But let's assume this has gone unnoticed and in the middle of your massive system it's there. Then suddenly someone says that on the part of issuing the invoice there was a date of April 31st. You will then start doing a lot of debugging in the invoices, in the database, spending hours and hours trying to find the bug, until you finally get to this function. And then you fix 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 rightly rejected.

A few months later, someone complains that there was an error in the integration with the database's XML. You go look it up in the XML and you get a bunch of crazy error messages in the log, you keep tracking and debugging that all night and you find that something went wrong on August 31st. After several hours of debugging you go back to the valida_data function and finally realize that August is 30 and not 31.

Okay, you've already wasted two nights racking your brain over this feature. Is there nothing else wrong with her? Well, let's create some tests to find out:

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 testa_valida_data() function, it will throw an error! This gives you the certainty that there is something wrong with the valida_data() function. So you go to the function and move it. To know if what you did is right, just run testa_valida_data() more time. When the function testa_valida_data() does not throw an error, then perhaps the function valida_data is right.

Why maybe? Because if the unit test passes, there's no guarantee that there aren't any more forgotten details. On the other hand if it fails then you are sure there is something wrong and the test will already tell you what is wrong and where the error came from, saving you a lot of debugging time. Also, while unit testing cannot guarantee you that your code is correct, the more and better the tests, the less likely it is that something that is wrong could have been missed.

And what about QUnit? QUnit is a framework that gives you flexibility and manageability in testing. After all, if we keep things as they are in testa_valida_data() and there are several errors, it will only show the first one and stop. Also, if there are several functions or modules to test, we still have to call the test functions manually. With QUnit this is mitigated. See 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 tests above, you will see that many of them fail. The valida_data() function I provided is purposely failed, and just click the " ► Run " button above to see that.

If you copy and paste this snippet somewhere else and change the valida_data() function, you can verify that 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 tests pass. I leave this task as an (easy) exercise for the reader. 🙂

So let's assume you've changed the code of the function until all tests pass. If sometime after that (possibly years later) you for some reason have to change the function (eg found a shape with better performance), then how do you know you haven't messed up or introduced a bug? Just click on the " ► Run " button and see the result. If you've introduced an error, it's likely to show up there. If the test did not report any errors, your changes are likely valid and reliable. And so you can have a quick, complete, efficient and detailed test with just one click.

Also, if years later someone finds a bug that wasn't covered in any tests, 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, existing tests have a high probability of reporting it.

On the other hand, to be blunt, there is 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 that time later with debugging that you wo n't need to do. Also, having good and broad test coverage is difficult, but even so, the greater the test coverage, the less likely that some code change will break something unexpectedly.

And of course, it's possible (and quite common) for tests to be wrong, which results in tests accepting wrong code, or rejecting right code, or simply not adequately testing what they set out 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 testing problems can have a variety of causes, such as: poor quality of the tested code; poor quality of test code; low test coverage of tested code; and confusing, ill-defined project requirements (how are you going to properly test something that you don't even know exactly what you're supposed to do?). Anyway, writing good tests also requires some experience, but to have experience, there is only one way: get your hands dirty and practice.

Link where to get QUnit: https://qunitjs.com/

Scroll to Top