diff --git a/Example/TestForVersion_0.1.1.js b/Example/TestForVersion_0.1.1.js new file mode 100644 index 0000000..f6ea437 --- /dev/null +++ b/Example/TestForVersion_0.1.1.js @@ -0,0 +1,471 @@ +/* +Testing script for testing the new changes introduced in UnitTestingApp script on version 0.1.1 +*/ + +// jshint esversion: 8 +if (typeof require !== 'undefined') { + UnitTestingApp = require('./UnitTestingApp.js'); +} + +// Function to test +function addTwoValues(a, b) { + return a + b; +} + +// Function to test in case an error is thrown +function addTwoValuesSafe(a, b) { + if (("number" != typeof a) || ("number" != typeof b)) throw new Error("Input argument is not a valid number"); + return addTwoValues(a, b); +} + +/***************** + * TESTS + * Taking the sources files from: https://github.com/WildH0g/UnitTestingApp + *****************/ + +/** + * Runs the tests; insert online and offline tests where specified by comments + * @returns {void} + */ +function runTests() { + const test = new UnitTestingApp(); + test.enable(); + test.clearConsole(); + test.runInGas(true); + + /************************ + * Run Local Tests Here + ************************/ + + test.printHeader("Testing addTwoValues using assertEquals"); + // We haven´t set levelInfo, so we are using the default value: 1 + test.printSubHeader("Using default Level Info value (1) for 'addTwoValues' function"); + test.printSubHeader("Expected: Test 1 pass user message, Test 2 pass with default, Test 3 fail user message, Test 4 fails with default message"); + test.assertEquals(() => addTwoValues(2, 2), 4, "Valid case: 2 + 2 = 4"); // Pass + test.assertEquals(() => addTwoValues(1, 2), 3); // Pass + test.assertEquals(() => addTwoValues(1, 2), 4, "Expected to fail, because 1 + 2 != 4"); // Fail + test.assertEquals(() => addTwoValues(1, 2), 4); // Fail + test.printSubHeader("Expected: 4-Test, 2-Tests fail, 2-Tests Pass"); + test.printSummary(); // It should print final result and statistics (two lines); + + test.printSubHeader("Testing when condition is boolean"); + test.printSubHeader("Reset counters"); + test.resetTestCounters(); + test.printSubHeader("Expected Test-1 pass with user message, Test-2 pass with default message, Test-3 fail with user message, Test-4 fail with default message"); + test.assertEquals(1 + 2 == 3, true, "Valid Case"); // Pass + test.assertEquals(1 + 2 == 3, true); // Pass + test.assertEquals(1 + 1 == 3, true, "Invalid Case"); + test.assertEquals(1 + 1 == 3, true); // Fail + test.printSubHeader("Expected: 4-Tests, 2-Tests fail, 2-Tests Pass"); + test.printSummary(); + + test.printSubHeader("Testing using strings conditions and validating the result with boolean condition"); + test.printSubHeader("Test-5 pass with user message, Test-6 pass with default message, Test-7 pass with user message, Test-8 fail with default message, Test-9 fail with deafult message"); + test.assertEquals("world" == "world", true, "Expected to pass 'world' = 'world'"); // Pass + test.assertEquals("world" == "world", true); // Pass + test.assertEquals("world" != "World", true, "Expected to pass 'world' != 'World'"); // Pass + test.assertEquals("world" == "World", true); // Fail + test.assertEquals("world" != "world", true); // Fail + test.printSubHeader("Expected: 9-Tests, 4-Tests fail, 5-Tests Pass"); + test.printSummary(); + + test.printSubHeader("Testing using empty message"); + test.printSubHeader("Reset counters"); + test.resetTestCounters(); + test.assertEquals(1 + 2 == 3, true, ""); // Pass + test.assertEquals(1 + 1 == 3, true, ""); // Fail + test.printSubHeader("Expected: 2-Tests, 1-Test fail, 1-Test Pass"); + test.printSummary(); + + test.printSubHeader("Testing using strings and validating with string value"); + test.printSubHeader("Reset counters"); + test.resetTestCounters(); + test.printSubHeader("Test-1 pass with user message, Test-2 pass with default message, Test-3 fail with user message, Test-4 fail with default message") + test.assertEquals("world", "world", "Expected to pass 'world' = 'world'"); // Pass + test.assertEquals("world", "world"); // Pass + test.assertEquals("world", "World", "Expected to fail 'world' != 'World'"); // Fail + test.assertEquals("world", "World"); // Fail + test.printSubHeader("Expected: 4-Tests, 2-Tests fail, 2-Tests Pass"); + test.printSummary(); + + test.printSubHeader("Testing undefined input arguments using default message or undefined message"); + test.printSubHeader("Reset counters"); + test.resetTestCounters(); + test.printSubHeader("Expected: Test-1 pass (no arguments), Test-2 fail (only condition), Test-3 pass with undefined message, Test-4 fail with undefined message"); + test.assertEquals(); // PASS both are undefined + test.assertEquals(1 + 1); // FAIL, because the expectedValue is undefined + test.assertEquals(1 + 1, 2, undefined); // Pass but message is undefined + test.assertEquals(1 + 1, 3, undefined); // Fail with undefined message (treated as null, so it shows default message + test.printSubHeader("Expected: 4-Tests, 2-Tests fail, 2-Tests pass"); + test.printSummary(); + + test.printSubHeader("Testing an unexpected error occurred"); + test.printSubHeader("Reset counters"); + test.resetTestCounters(); + test.printSubHeader("Expected: unexpected error with user message"); + test.assertEquals( + () => addTwoValuesSafe("a", "b"), // an unexpected error will thrown + "a + b", + "Expected: unexpected error" + ); + test.printSubHeader("Expected: unexpected error with default message"); + test.assertEquals( + () => addTwoValuesSafe("a", "b"), // an unexpected error will thrown + "a + b" + ); + + // Testing the existing assert function work as expected for backward compatibility + test.printHeader("Testing addTwoValues using assert"); + test.printSubHeader("Test-1 Pass with user message, Test-2 pass with default message, Test-3 fail with user message, Test-4 fail with default message"); + test.assert(() => addTwoValues(1, 2) == 3, "Valid case: 1 + 2 = 3"); // Pass + test.assert(() => addTwoValues(1, 2) == 3); // Pass + test.assert(() => addTwoValues(1, 2) == 4, "Invalid case: 1 + 2 != 4"); // Fail + test.assert(() => addTwoValues(1, 2) == 4); // Fail + test.printSubHeader("4-Tests, 2-Tests Fail, 2-Tests Pass"); + test.printSummary(); + + test.printSubHeader("Testing when condition is boolean"); + test.printSubHeader("Reset counters"); + test.resetTestCounters(); + test.printSubHeader("Test-1 Pass with user message, Test-2 pass with default message, Test-3 fail user message, Test-4 fail with default message"); + test.assert(1 + 2 == 3, "Valid case: 1 + 2 = 3"); // Pass + test.assert(1 + 2 == 3); // Pass + test.assert(1 + 1 == 3, "Invalid case: 1 + 1 != 3"); // Fail + test.assert(1 + 1 == 3); // Fail + test.printSubHeader("4-Tests, 2-fail, 2-Tests pass"); + test.printSummary(); + + + test.printSubHeader("Testing using strings conditions and validating the result with boolean condition"); + test.printSubHeader("Test-5 pass with user message, Test-6 pass with default message, Test-7 pass with user message, Test-8 fail with user message, Test-9 fail with deafult message, Test-10 fail with default message"); + test.assert("world" == "world", "Expected to pass 'world' = 'world'"); // Pass + test.assert("world" == "world"); // Pass + test.assert("world" != "World", "Expected to pass 'world' != 'World'"); // Pass + test.assert("world" == "World", "Expected to fail 'word' != 'World"); // Fail + test.assert("world" == "World"); // Fail + test.assert("world" != "world"); // Fail + + test.printSubHeader("Expected: 10-Tests, 5-Tests fail, 5-Tests Pass"); + test.printSummary(); + + test.printSubHeader("Testing using empty message"); + test.printSubHeader("Reset counters"); + test.resetTestCounters(); + test.assert(1 + 2 == 3, ""); // Pass + test.assert(1 + 1 == 3, ""); // Fail + test.printSubHeader("Expected: 2-Tests, 1-Test fail, 1-Test Pass"); + test.printSummary(); + + test.printSubHeader("Testing undefined input arguments"); + test.printSubHeader("Reset counters"); + test.resetTestCounters(); + test.printSubHeader("Expected: Test-1 Fail (no arguments), Test-2 Pass with (undefined message), Test-3 Pass(with undefined message), Test-4 Fail (with undefined message)"); + test.assert(); // Fail, not a valid condition + test.assert(undefined == undefined); // Pass + test.assert(1 + 1 == 2); // Pass + test.assert(1 + 1 == 3); // Fail + test.printSubHeader("Expected: 4-Tests, Fail=2, Pass=2"); + test.printSummary(); + + test.printSubHeader("Testing an unexpected error occured"); + test.printSubHeader("Reset counters"); + test.resetTestCounters(); + test.printSubHeader("Expected: unexpected error with user message"); + test.assert( + () => addTwoValuesSafe("a", "b"), // unexpected error was thrown + "Expected: unexpected error" + ); + test.printSubHeader("Expected: unexpected error with default message"); + test.assert( + () => addTwoValuesSafe("a", "b") // unexpected error was thrown + ); + + test.printSubHeader("Testing set levelInfo"); + test.printSubHeader("Reset counters"); + test.resetTestCounters(); + test.assert(() => test.levelInfo = 1.1, "Valid case: 1.1 is a number"); + test.catchErr( + () => test.levelInfo = "non number", // Expected to throw a TypeError + "Input argument value should be a number", // this is the error message we are expecting + "Throw an error because value is a string. Expected error type and message to be correct", + TypeError + ); + test.printSubHeader("2-Tests, 0-fail, 2-pass"); + test.printSummary(); + + test.printHeader("Testing is2Array"); + test.printSubHeader("Reset counters"); + test.resetTestCounters(); + + const array2D = [ + ["a1", "a2"], + ["b1", "b2"] + ]; + const array = ["a1", "a2"]; + test.is2dArray(array2D, "Expected to pass, values is an array of arrays"); + test.is2dArray(null, "Expected to fail, it is null input value"); + test.is2dArray(array, "Expected to fail, it is a single array"); + test.is2dArray([array], "Expected to pass, it is a 2D array"); + test.is2dArray([array]); // PASS + test.is2dArray(array); // FAIL + test.is2dArray(addTwoValuesSafe(1, 2)); // FAIL + test.is2dArray(() => addTwoValuesSafe("a", "b")); // FAIL + test.printSubHeader("8-Tests, 5-fail, 3-pass"); + test.printSummary(); + + test.printHeader("Testing catching errors using catchErr"); + test.printSubHeader("Reset counters"); + test.resetTestCounters(); + + test.printSubHeader("Testing backward compatibility"); + + test.printSubHeader("Expected to pass: throw an error and error message is correct, user provided the message"); + test.catchErr( + () => addTwoValuesSafe("a", "b"), // we’re passing a string here to test that our function throws an error + "Input argument is not a valid number", // this is the error message we are expecting (correct) + "We caught the error message correctly" // This is the user message to log out + ); + + test.printSubHeader("Expected to pass: throw an error and error message is correct, using default message"); + test.catchErr( + () => addTwoValuesSafe("a", "b"), // we’re passing a string here to test that our function throws an error + "Input argument is not a valid number", // this is the error message we are expecting (correct) + ); + + test.printSubHeader("Expected to fail: wrong error message, user provided the message"); + test.catchErr( + () => addTwoValuesSafe("a", "b"), // we’re passing a string here to test that our function throws an error + "Wrong error message", // this is the error message we are expecting + "We caught the error, but with the wrong error message" + ); + + test.printSubHeader("Expected to fail: wrong error message, using default message"); + test.catchErr( + () => addTwoValuesSafe("a", "b"), // we’re passing a string here to test that our function throws an error + "Wrong error message" // this is the error message we are expecting (wrong) + ); + + test.printSubHeader("Expected: 4-Tests, 2-Tests fail, 2-Pass"); + test.printSummary(); + + test.printSubHeader("Testing error type via errorType optional input argument"); + test.printSubHeader("Reset counters"); + test.resetTestCounters(); + + test.printSubHeader("Expected to pass: error type and error message are correct, user provided the message"); + test.catchErr( + () => addTwoValuesSafe("a", "b"), // we’re passing a string here to test that our function throws an error + "Input argument is not a valid number", // this is the error message we are expecting + "We caught the error type and error message correctly", + Error // This is the error type we are expecting + ); + + test.printSubHeader("Expected to pass: error type and error message are correct, using default message"); + test.catchErr( + () => addTwoValuesSafe("a", "b"), // we’re passing a string here to test that our function throws an error + "Input argument is not a valid number", // this is the error message we are expecting + Error // This is the error type we are expecting + ); + + test.printSubHeader("Expected to fail: error type correct, but wrong error message, user provided the message"); + test.catchErr( + () => addTwoValuesSafe("a", "b"), // we’re passing a string here to test that our function throws an error + "Wrong error message", // this is the error message we are expecting + "We caught the error type correctly, but wrong error message", + Error // This is the error type we are expecting + ); + + test.printSubHeader("Expected to fail: error type correct, but wrong error message using default message"); + test.catchErr( + () => addTwoValuesSafe("a", "b"), // we’re passing a string here to test that our function throws an error + "Wrong error message", // this is the error message we are expecting + Error // This is the error type we are expecting + ); + + test.printSubHeader("Expected to fail: wrong error type, error message correct, user provided the message"); + test.catchErr( + () => addTwoValuesSafe("a", "b"), // we’re passing a string here to test that our function throws an error + "Input argument is not a valid number", // this is the error message we are expecting + "We caught incorrect error type, but the error message is correct", + TypeError // This is the error type we are expecting + ); + + test.printSubHeader("Expected to fail: wrong error type, error message correct, using default message"); + test.catchErr( + () => addTwoValuesSafe("a", "b"), // we’re passing a string here to test that our function throws an error + "Input argument is not a valid number", // this is the error message we are expecting + TypeError // This is the error type we are expecting + ); + + test.printSubHeader("Expected to fail: wrong error type and error message, user provided the message"); + test.catchErr( + () => addTwoValuesSafe("a", "b"), // we’re passing a string here to test that our function throws an error + "Wrong error message", // this is the error message we are expecting + "We caught incorrect error type and message", + TypeError // This is the error type we are expecting + ); + + test.printSubHeader("Expected to fail: wrong error type and error message, using default message"); + test.catchErr( + () => addTwoValuesSafe("a", "b"), // we’re passing a string here to test that our function throws an error + "Wrong error message", // this is the error message we are expecting + TypeError // This is the error type we are expecting + ); + + test.printSubHeader("Expected: 8-Tests 6-Tests fail, 2-Pass"); + test.printSummary(); + + test.printSubHeader("Testing no error should be thrown"); + test.printSubHeader("Reset counters"); + test.resetTestCounters(); + + // Not catching error + test.printSubHeader("Expected to fail: no error should be thrown, correct error message, user provided the message"); + test.catchErr( + () => addTwoValuesSafe(1, 2), // we are passing valid values (no error thrown) + "Input argument is not a valid number", // this is the error message we are expecting + "No error should be thrown" // this is the error message we are expecting + ); + + test.printSubHeader("Expected to fail: no error should be thrown, correct error message using defeault message"); + test.catchErr( + () => addTwoValuesSafe(1, 2), // we are passing valid values (no error thrown) + "Input argument is not a valid number" // this is the error message we are expecting + ); + + test.printSubHeader("Expected to fail: no error should be thrown, wrong error message, user provided the message"); + test.catchErr( + () => addTwoValuesSafe(1, 2), // we are passing valid values (no error thrown) + "No error", // this is the error message we are expecting + "No error should be thrown" // this is the error message we are expecting + ); + + test.printSubHeader("Expected to fail: no error should be thrown, using default message"); + test.catchErr( + () => addTwoValuesSafe(1, 2), // we are passing valid values (no error thrown) + "No error", // this is the error message we are expecting + ); + + test.printSubHeader("Expected to fail: no error should be thrown, error type and message are correct, user provided the message"); + test.catchErr( + () => addTwoValuesSafe(1, 2), // we are passing valid values (no error thrown) + "Input argument is not a valid number", // this is the error message we are expecting + "No error should be thrown", // this is the error message we are expecting + Error + ); + + test.printSubHeader("Expected to fail: no error should be thrown, error type and message are correct, using default message"); + test.catchErr( + () => addTwoValuesSafe(1, 2), // we are passing valid values (no error thrown) + "Input argument is not a valid number", // this is the error message we are expecting + Error + ); + + test.printSubHeader("Expected to fail: no error should be thrown, error type correct and wrong error message, user provided the message"); + test.catchErr( + () => addTwoValuesSafe(1, 2), // we are passing valid values (no error thrown) + "No error", // this is the error message we are expecting + "No error should be thrown", // this is the error message we are expecting + Error + ); + + test.printSubHeader("Expected to fail: no error should be thrown, error type correct and wrong error message, using default message"); + test.catchErr( + () => addTwoValuesSafe(1, 2), // we are passing valid values (no error thrown) + "No error", // this is the error message we are expecting + Error + ); + + test.printSubHeader("Expected to fail: no error should be thrown, providing wrong errorType and correct error message, user provided the message"); + test.catchErr( + () => addTwoValuesSafe(1, 2), // we are passing valid values (no error thrown) + "Input argument is not a valid number", // this is the error message we are expecting + "No error should be thrown", // this is the error message we are expecting + TypeError + ); + + test.printSubHeader("Expected to fail: no error should be thrown, providing wrong errorType and correct error message, using default message"); + test.catchErr( + () => addTwoValuesSafe(1, 2), // we are passing valid values (no error thrown) + "Input argument is not a valid number", // this is the error message we are expecting + TypeError + ); + + test.printSubHeader("Expected to fail: no error should be thrown, wrong error type an message, user provided the message"); + test.catchErr( + () => addTwoValuesSafe(1, 2), // we are passing valid values (no error thrown) + "No error", // this is the error message we are expecting + "No error should be thrown", // this is the error message we are expecting + TypeError + ); + + test.printSubHeader("Expected to fail: no error should be thrown, providing wrong errorType and wrong error message using default message"); + test.catchErr( + () => addTwoValuesSafe(1, 2), // we are passing valid values (no error thrown) + "No error", // this is the error message we are expecting + TypeError + ); + + test.printSubHeader("Expected: 12-Tests, 12-Tests fail, 0-Pass"); + test.printSummary(); + + // Testing similar basic tests but in silent mode (levelInfo = 0) + test.printHeader("Testing assert and assertEquals under silent mode (levelInfo = 0)"); // It logs out, because we haven't changed levelInfo yet + test.levelInfo = 0; // 0-Only summary result, 1-Detail results + // Because levelInfo = 0 we use the console for traceability purpose + console.log("levelInfo = " + test.levelInfo); + console.log("Reset counters"); + test.resetTestCounters(); + console.log("Testing assertEquals"); + console.log("********************"); + test.assertEquals(() => addTwoValues(2, 2), 4, "Valid case: 2 + 2 = 4"); // Pass + test.assertEquals(() => addTwoValues(1, 2), 3); // Pass + test.assertEquals(() => addTwoValues(1, 2), 4, "Expected to fail, because 1+2 != 4"); // Fail + test.assertEquals(() => addTwoValues(1, 2), 4); // Fail + console.log("Expected: Some tests failed (one line)"); + test.printSummary(); // Only summary result: Some test failed + console.log("Reset counters"); + test.resetTestCounters(); + console.log("Shows only the summary line, the counters are reseted so no tests, therefore all test passed"); + test.printSummary(); //Shows only summary line + + console.log("Testing assert"); + console.log("**************"); + console.log("Reset counters"); + test.resetTestCounters(); + test.assert(() => addTwoValues(1, 2) == 3, "Valid case: 1 + 2 = 3"); // Pass + test.assert(() => addTwoValues(1, 2) == 4, "Invalid case: 1 + 2 != 4"); // Fail + console.log("Expected to see some test failed"); + test.printSummary(); + + // Reset the counters. For testing all test passed under silent mode + console.log("Testing the case all test passed with silent mode"); + console.log("Reseting the counters"); + test.resetTestCounters(); + console.log('Testing with assert, under silent mode: one test executed and passed'); + test.assert(() => addTwoValues(1, 2) == 3, "Valid case"); + console.log("Printing the summary line only: all test passed"); + test.printSummary(); + + console.log("Changing the level info to 1"); + test.levelInfo = 1; + test.printSubHeader("Showing now printSummary with two lines from the previous set of tests"); + test.printSummary(); + test.printSubHeader("Set levelInfo to 1.1"); + test.levelInfo = 1.1; + test.printSubHeader("Showing now printSummary with two lines from the previous set of tests"); + test.printSummary(); + test.printSubHeader("Set levelInfo to 0.1"); + test.levelInfo = 0.1; + console.log("Showing now printSummary with the summary line only from the previous set of tests"); + test.printSummary(); + console.log("Changing the level info to 1"); + test.levelInfo = 1; + + test.printHeader("Testing enable = false, the print-family functions print no information"); + test.disable(); + test.printHeader("No expected output"); + test.printSubHeader("No expected output"); + test.printSummary(); + +} diff --git a/README.md b/README.md index 6710813..d9254bc 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ if(test.isEnabled) { test.disable(); // tests will not run below this line ``` +**Note:** If tests are disable, print-family function will no produce any output regardless of the level of information we have configured to show. See section: *Control Level of Information to log out* for more information. ### Choosing the Environment with runInGas(Boolean) @@ -46,40 +47,254 @@ test.runInGas(true); // switch to online tests in Google Apps Script environment ``` -Then we have the actual built-in testing methods, `assert()`, `catchErr()` and `is2dArray()`. +Then we have the actual built-in testing methods, `assert()`, `catchErr()` and `is2dArray()`, etc.. -### assert(condition, message) +### Control Level of Information to log out -`assert()` is the main method of the class. The first argument that it takes is the condition and it checks whether the condition is truthy or falsy. The condition can either be a boolean value or a function that returns such a condition, function being the preferred approach for error catching reasons. If the condition is truthy, it logs out a “PASSED” message, otherwise it logs out a “FAILED” message. If you pass in a function that throws an error, the method will catch the error and log out a “FAILED” message. For example: +The function attribute `levelInfo` controls the level information to be reported through the console. If `value`is less or equal than `0`, it runs in silent mode, i.e. no information will shown in the console. The only exception from print-family functions is `printSummary()` it logs out a single summary line. For example if all tests passed, the output will be: + +```ALL TESTS ✔ PASSED``` + +if at least one test failed then it logs out the following information: + +```❌ Some Tests FAILED``` + +This setup is usufull for large tests where we just want to add some incremental test and to have just a minimal output about the overall testing result. + +Here is how the level of information can be specified for silent mode: ```javascript -const num = 6; +test.levelInfo = 0; +``` +If the value is `1`(default value) or greater, it means trace information will log out the result per each test, indicating if the test failed or passed. Depending on the specific testing function it will log out different information, for example, let's says we want to test the following custom function: -test.assert(() => num % 2 === 0, `Number ${num} is even`’); -// logs out PASSED: Number 2 is even +```javascript +function addTwoValues (a, b) { + return a + b; +} +``` + +then invoking the following assert functions: + +```javascript +test.assert(() => addTwoValues(1,2)==3); // expected result is 3 +test.assertEquals(() => addTwoValues(1,2),3); // expected result is 3 +``` + +will return + +``` +✔ PASSED: Input argument 'condition' passed +✔ PASSED: 3 === 3 +``` + +on contrary, if we invoke the functions with a wrong expected result: + +```javascript +test.assertEquals(() => addTwoValues(1,2),4); // expected result is 4 +test.assertEquals(() => addTwoValues(1,2),4); // expected result is 4 +``` + +will return + +``` +❌ FAILED: Input argument 'condition' failed +❌ FAILED: 3 != 4 +``` +In case of fail when the user doesn't provide the input argument `message`, `assertEquals` indicates the specific mismatch. + +If we invoke `assertEquals` or `assert` including its optional input argument `message`, the user has more control of the information that goes to the console: + +```javascript +test.assert(() => addTwoValues(1,2)==3, "Expected result: 1 + 2 = 3"); +test.assert(() => addTwoValues(1,2)==4, "Wrong result because 1 + 2 should be equal to 3"); +test.assertEquals(() => addTwoValues(1,2),3, "Expected result: 1 + 2 = 3"); +test.assertEquals(() => addTwoValues(1,2),4, "Wrong result because 1 + 2 should be equal to 3"); +``` + +The output will be: + +``` +✔ PASSED: Expected result: 1 + 2 = 3 +❌ FAILED: Wrong result because 1 + 2 should be equal to 3 +✔ PASSED: Expected result: 1 + 2 = 3 +❌ FAILED: Wrong result because 1+2 should be equal to 3 +``` + +If we invoke `printSummary()` with `levelInfo` is equal or greater than `1`, it logs out an additional line providing statistics about testing results: + +``` +TOTAL TESTS= 4, ❌ FAILED=2, ✔ PASSED=2 +❌ Some Tests FAILED +``` +Indicating that `2`test passed and `2` test failed and the total tests executed + +### assert(condition, message = null) +`assert()` is the main method of the class. The first argument that it takes is the condition and it checks whether the condition is truthy or falsy. The condition can either be a boolean value or a function that returns such a condition, function being the preferred approach for error catching reasons. If the condition is truthy, it logs out a “PASSED” message, otherwise it logs out a “FAILED” message. If you pass in a function that throws an error, the method will catch the error and log out a “ERROR” message and it counts as a fail test. For example: + +```javascript +const num = 6; +test.assert(() => num % 2 === 0, `Number ${num} is even`’); test.assert(() => num > 10, `Number ${num} is bigger than 10`); -// logs out FAILED: Number 2 is bigger than 10 ``` +logs out: +``` +✔ PASSED: Number 6 is even +❌ FAILED: Number 6 is bigger than 10 +``` + +Let's say we have the following function that throws an error: -### catchErr(condition, expectedErrorMessage, message) +```javascript +function addTwoValues(a, b) { + if (("number" != typeof a) || ("number" != typeof b)) + throw new Error("Input argument is not a valid number"); + return a+b; +} +``` +then when testing `addTwoValues`: + +```javascript +test.assert(() => addTwoValues("a", "b") === 0, "Expected an error was thrown"); +``` + +will show an error message in the console as follow. For errors the library uses `console.error` so it will appear with light red background + +``` +❌ ERROR: Expected and error was thrown (Error: Input argument is not a valid number) +``` -The goal of this method is to test whether your callback function (`callback`) catches the correct error. What you want to do is to make sure that the callback actually throws an error and then the `tests` method will check if it’s the correct one. Then finally it will use `assert()` to log out the corresponding message. For example, let’s say you have a function that returns the square value of a number you pass as the argument, and you want to throw an error if the argument isn’t a number, then you can test the error this way: +**Note:** If input argument `message` is not provided a default built-in message is provided indicating the `condition` passed or failed. + +### assertEquals(condition, expectedResult, message = null) +Similar to `assert` function, handy for checking against specific expected result, `assert` function when input argument `message` is not provided, there is not way to verify the condition againts expected result, with `assertEquals` the buil-in default message helps to confirm the match or to identify the mismatch. Here is how to validate javascript standard `max()`function: + + +```javascript +test.assertEquals(() => Math.max(1,2),2); // Pass +test.assertEquals(() => Math.max(1,2),1); // Fail +test.assertEquals(() => Math.max(1,2),2, "Correct result the max of 1,2 is 2"); // Pass +``` +``` +✔ PASSED: 2 === 2 +❌ FAILED: 2 != 1 +✔ PASSED: Correct result the max of 1,2 is 2 +``` +As in `assert` function in case an error is thrown an error message will be generated. + +**Note:** Basic types values such as `number`, `boolean`, `null` or `undefined` are not wrapped in quotes ('), all other types are wrapped. + +### catchErr(callback, errorMessage, message = null, errorType = null) + +The goal of this method is to test whether your callback function (`callback`) catches the correct error message and/or error type. What you want to do is to make sure that the callback actually throws an error and then if it’s the correct one. Then finally it will use `assert()` to log out the corresponding message. For example, let’s say you have a function that returns the square value of a number you pass as the argument, and you want to throw an error if the argument isn’t a number, then you can test the error this way: ```javascript function square(number) { - if (typeof number !== ‘number’) throw new Error(‘Argument must be a number’); + if ("number" != typeof number) throw new Error("Argument must be a number"); return number * number; } test.catchErr( - () => square(‘a string’), // we’re passing a string here to test that our function throws an error - ‘Argument must be a number’, // this is the error message we are expecting - ‘We caught the type error correctly’ + () => square("a string"), // we’re passing a string here to test that our function throws an error + "Argument must be a number", // this is the error message we are expecting + "We caught the type error correctly" +); +``` +If message is not provided a bult-in message is provided as follow for previous example: +```javascript +test.catchErr( + () => square("a string"), // we’re passing a string here to test that our function throws an error + "Argument must be a number" // this is the error message we are expecting ); ``` +``` +✔ PASSED: Error message is correct +``` -### is2dArray(Array) +Similar for fail case: +```javascript +test.catchErr( + () => square("a string"), // we’re passing a string here to test that our function throws an error + "Wrong error message" // this is the error message we are expecting +); +``` +``` +❌ FAILED: Wrong error message: 'Argument must be a number' != 'Wrong error message' +``` +When input argument `errorType` is provided it also checks the error type caught is the same as `errorType`. For example: + +```javascript +// Correct error type and error message +test.catchErr( + () => square("a string"), // we’re passing a string here to test that our function throws an error + "Argument must be a number", // this is the error message we are expecting + Error // This is the error type we are expecting +); +// Wrong error type and correct error message +test.catchErr( + () => square("a string"), // we’re passing a string here to test that our function throws an error + "Argument must be a number", // this is the error message we are expecting + TypeError // This is the error type we are expecting +); +// Correct error type and wrong error message +test.catchErr( + () => square("a string"), // we’re passing a string here to test that our function throws an error + "Wrong error message", // this is the error message we are expecting + Error // This is the error type we are expecting +); +// Wrong error type and error message +test.catchErr( + () => square("a string"), // we’re passing a string here to test that our function throws an error + "Wrong error message", // this is the error message we are expecting + TypeError // This is the error type we are expecting +); + +``` + +it will produce the following output: + +``` +✔ PASSED: Error type and error message are correct +❌ FAILED: Wrong error type: 'Error' != 'TypeError' +❌ FAILED: Wrong error message: 'Argument must be a number' != 'Wrong error message' +❌ FAILED: Wrong error type: 'Error' != 'TypeError' and wrong error message: 'Argument must be a number' != 'Wrong error message' +``` + +If we pass a valid value, no error is thrown: + +```javascript +test.catchErr( + () => square(2), // we’re passing a valid value + "Argument must be a number", // this is the error message we are expecting + Error +); +``` + +the test fails with the following information + +``` +❌ FAILED: No error thrown +``` + +**Note:** Even though you can invoke both asserts functions (`assert`, `assertEquals`) not using the `=>` (arrow function), for example: + +```javascript +test.assertEquals(square(2), 4); +test.assert(square(2)==4, "Valid case sqrt(2) is equal to 4"); +``` + +We don't recommend to do it, because `catchErr()` function will require arrow function invokation. The following code will produce an execution error on the first line of the body of `sqrt` function. + +```javascript +test.catchErr( + square("a string"), // we’re passing a string here to test that our function throws an error + "Argument must be a number", // this is the error message we are expecting + "We caught the type error correctly" + ); +``` + +### is2dArray(Array, message = null) This method runs a test to check whether the argument is a 2D array. This is useful for testing spreadsheet values before inserting them into a spreadsheet: @@ -90,24 +305,72 @@ const values = [ ]; test.is2dArray(values, ‘values is an array of arrays’); // logs out PASSED + ``` +If input argument `message` is not provided a built-in message is generated indicated the test passed or failed. Then there are a couple of helper methods, `printHeader()` and `clearConsole()`. -### printHeader(headerStr) +### Print-family Functions + +**Note**: The level of information shown by print-family functions will depend on the level of information the user specified via `levelInfo`, or if no value was specified it assumes the level of information is `1`. See section: *Control Level of Information to log out* for more information. + +The `printHeader()` function just helps with readability by printing a header in the console like this. It can be used for printing for example the title of the testing set. Here the expected result under `1` level of information: -Just helps with readability by printing a header in the console like this: ```javascript test.printHeader(‘Offline tests’); -/* +``` + Logs out the following: -********************* + +``` +*************** * Offline tests -********************* -*/ +*************** +``` + +There also a second print header function: `printSubHeader(text)`, usefull to log out a sub header as a single line with prefix `**`. Here the output under level of information equal to `1`: + +```javascript +test.printSubHeader(‘Testing valid cases...’); +``` + +logs out: + +``` +** Testing valid cases... +``` + +There is a third print function: `printSummary()`, that logs out a summary of testing results (depending on the level of information we want to show) + +```javascript +test.printSummary(); +``` + +If we ran 20 tests, where there is one failed test, under level of information equal to `1`, the result will be: + +``` +TOTAL TESTS= 20, ❌ FAILED=1, ✔ PASSED=19 +❌ Some Tests FAILED +``` + +Similarly if the `levelInfo` lower or equial than `0`, the output will be: + +``` +❌ Some Tests FAILED +``` + +on contrary if all tests passed, will show: ``` +ALL TESTS ✔ PASSED +``` + + +### resetTestCounters() +The `resetTestCounters()` is usefull for reseting testing counters (`_nTests`, `_nFailTests`, `_nPassTests`, private attributes of the class `UnitTestingApp`), the function `printSummary()` will log out information depending on the overall testing results based on such attributes. We can use this function to reset testing counters after a running a set of tests, so we can print a summary information per set of tests using `printSummary()`. + ### clearConsole() @@ -183,4 +446,4 @@ function runTests() { ## Current Version -0.1.0 +0.1.1 diff --git a/UnitTestingApp.js b/UnitTestingApp.js index 3b2e36f..ce35434 100644 --- a/UnitTestingApp.js +++ b/UnitTestingApp.js @@ -5,22 +5,33 @@ ************************/ /** - * Class for running unit tests + * Class for running unit tests. For more information check the following links: + * https://github.com/WildH0g/UnitTestingApp + * https://medium.com/geekculture/taking-away-the-pain-from-unit-testing-in-google-apps-script-98f2feee281d */ - let UnitTestingApp = (function () { - +let UnitTestingApp = (function () { + // Using WeakMap to keep attributes private, idea taken from here: + // https://chrisrng.svbtle.com/using-weakmap-for-private-properties const _enabled = new WeakMap(); const _runningInGas = new WeakMap(); + const _nTests = new WeakMap(); // Total number of tests executed + const _nFailTests = new WeakMap(); // Total tests failed + const _nPassTests = new WeakMap(); // Total tests passed + const _levelInfo = new WeakMap(); // Level of information to show in the console (0-summary, 1-trace and test result information) class UnitTestingApp { constructor() { if (UnitTestingApp.instance) return UnitTestingApp.instance; - + _enabled.set(this, false); _runningInGas.set(this, false); + _levelInfo.set(this, 1); + _nTests.set(this, 0); + _nFailTests.set(this, 0); + _nPassTests.set(this, 0); UnitTestingApp.instance = this; - + return UnitTestingApp.instance; } @@ -31,9 +42,9 @@ disable() { _enabled.set(this, false); } - + get isEnabled() { - return _enabled.get(this); + return _enabled.get(this); } get isInGas() { @@ -44,6 +55,15 @@ return _runningInGas.get(this); } + get levelInfo() { + return _levelInfo.get(this); + } + + set levelInfo(value) { + if ("number" !== typeof value) throw new TypeError("Input argument value should be a number"); + _levelInfo.set(this, value); + } + runInGas(bool = true) { _runningInGas.set(this, bool); } @@ -52,67 +72,198 @@ if (console.clear) console.clear(); } + stopIfNotActive_() {// Helper function (not vissible for users of the library) + if (!_enabled.get(this)) return true; + if (this.isInGas !== this.runningInGas) return true; + return false; + } + /** - * Tests whether conditions pass or not + * Reset statistics counters: Number of tests, test passed and test failed + * @return {void} + */ + resetTestCounters() { + _nTests.set(this, 0); + _nFailTests.set(this, 0); + _nPassTests.set(this, 0); + } + + /** + * Tests whether conditions pass or not. If other attributes such as enable, runningInGas indicate + * the test is not active, no test is carried out. * @param {Boolean | Function} condition - The condition to check - * @param {String} message - the message to display in the onsole + * @param {String} message - the message to display in the console (if attribute levelInfo >=1). + * if value is not provided (default) it builds a default message indicating whether the test + * failed os passed, or some error occurred. * @return {void} */ - assert(condition, message) { - if(!_enabled.get(this)) return; - if(this.isInGas !== this.runningInGas) return; + assert(condition, message = null) { + if (this.stopIfNotActive_()) return; + _nTests.set(this, _nTests.get(this) + 1); try { if ("function" === typeof condition) condition = condition(); - if (condition) console.log(`✔ PASSED: ${message}`); - else console.log(`❌ FAILED: ${message}`); - } catch(err) { - console.log(`❌ FAILED: ${message} (${err})`); + if (condition) { + _nPassTests.set(this, _nPassTests.get(this) + 1); + message = (message == null) ? "Input argument 'condition' passed" : message; + if (this.levelInfo >= 1) console.log(`✔ PASSED: ${message}`); + } else { + message = (message == null) ? "Input argument 'condition' failed" : message; + _nFailTests.set(this, _nFailTests.get(this) + 1); + if (this.levelInfo >= 1) console.log(`❌ FAILED: ${message}`); + } + } catch (err) { + message = (message == null) ? "Something was wrong" : message; + _nFailTests.set(this, _nFailTests.get(this) + 1); + if (this.levelInfo >= 1) console.error(`❌ ERROR: ${message} (${err})`); } } /** - * Tests functions that throw error messages - * @param {Function} callback - the function that you expect to return the error message - * @param {String} errorMessage - the error message you are expecting - * @param {String} message - the message to display in the console + * Tests whether condition result is strictly equal (===) to expected result or not. + * If other attributes such as enable, runningInGas + * indicate the test is not active no test is carried out. + * @param {Boolean | Function} Condition or fun - to check + * @param {String} expectedResult - The expected result to validate + * @param {String} message - If present, then used as message to display to console (if attribute levelInfo >= 1). + * If message is not provided (default), if test failed, i.e. result is not equal to expectedResult, + * it shows the missmatch in the form of: + * "'result' != 'expectedResult'" (numbers or booleans are not wrapped in quotes (')) + * If the test passed, the message will be: + * "'result' === 'expectedResult'" (numbers or booleans are not wrapped in quotes (')) + * If some error occured, then: "Something was wrong" * @return {void} */ - catchErr(callback, errorMessage, message) { - if(!_enabled.get(this)) return; - if(this.isInGas !== this.runningInGas) return; - let error; - let isCaught = false; + assertEquals(condition, expectedResult, message = null) { + if (this.stopIfNotActive_()) return; + _nTests.set(this, _nTests.get(this) + 1); + + // wraps in quotes (') any type except numbers, booleans, null or undefined + function q(v) {return ('number' === typeof v) || ('boolean' === typeof v) || !v ? v: `'${v}'`} + try { + if ("function" === typeof condition) condition = condition(); + let result = condition === expectedResult; + if (result) { + _nPassTests.set(this, _nPassTests.get(this) + 1); + message = (message == null) ? q(condition) + " === " + q(expectedResult) : message; + if (this.levelInfo >= 1) console.log(`✔ PASSED: ${message}`); + } else { + _nFailTests.set(this, _nFailTests.get(this) + 1); + message = (message == null) ? q(condition) + " != " + q(expectedResult) : message; + if (this.levelInfo >= 1) console.log(`❌ FAILED: ${message}`); + } + } catch (err) { + _nFailTests.set(this, _nFailTests.get(this) + 1); + message = (message == null) ? "Something was wrong" : message; + if (this.levelInfo >= 1) console.error(`❌ ERROR: ${message} (${err})`); + } + } + + /** + * Tests functions that throw error, validating message and/or type of error. If no error thrown, then the test fails. + * If other attributes such as enable, runningInGas indicate the test is not active, no test is carried out. + * @param {Function} callback - the function that you expect to return the error message + * @param {String} errorMessage - the error message you are expecting + * @param {String} message - the message to display to console (if attribute levelInfo >= 1). + * If null (default value), in case error is cautgh, it builds a predefined message as follow: + * In case of wrong error type: "Wrong error type: 'CaughErrorType' != 'errorType'" + * In case of wrong error message: "Wrong error message: 'Caugh error message' != 'errorMessage'" + * In case both errorType and errorMessage are wrong: + * "Wrong error type: 'CaughErrorType' != 'errorType' and wrong error message: 'Caugh error message' != 'errorMessage'" + *. In case error type and error message are correct, then: + * "Error type and error message are correct" + * If no error was caught, then the message will be: "No error thrown" and it is considered the test failed. + * @param {Type} errorType - the error type you are expecting. If null (default) the error type is not tested. + * @return {void} + */ + catchErr(callback, errorMessage, message = null, errorType = null) { + if (this.stopIfNotActive_()) return; + let isCaughtErrorMessage = false, isCaughtErrorType = true // Error type is optional so default result is true + + // Identify correct input argument by its expected type + if ((message != null) && ("string" != typeof message)) {// invoked: catchErr(callback,string, null, Error) + errorType = message; + message = null; + } + try { callback(); } catch (err) { - error = err; - isCaught = new RegExp(errorMessage).test(err); + if (errorType != null) isCaughtErrorType = err instanceof errorType; + isCaughtErrorMessage = new RegExp(errorMessage).test(err); + if (message == null) {// Building default message in case of fail + if(!isCaughtErrorType) message = `Wrong error type: '${err.name}' != '${errorType.name}'`; + if (!isCaughtErrorMessage){ + let msg = `error message: '${err.message}' != '${errorMessage}'`; + message = (isCaughtErrorType) ? `Wrong ${msg}` : `${message} and wrong ${msg}`; + } + } + // In case it didn't fail (message is still null), building default message + if(message == null) message = (errorType == null) ? "Error message is correct" : "Error type and error message are correct"; } finally { - this.assert(isCaught, message); + if (message == null) message = "No error thrown"; + this.assert(isCaughtErrorType && isCaughtErrorMessage, message); } } /** - * Tests whether an the argument is a 2d array + * Tests whether an the argument is a 2d array. If other attributes such as enable, runningInGas + * indicate the test is not active no test is carried out. * @param {*[][]} array - any 2d-array + * @param {String} message - The message to log out. If message is not provided a default + * message will be provided. * @returns {Boolean} */ - is2dArray(array, message) { - if(!_enabled.get(this)) return; - if(this.isInGas !== this.runningInGas) return; + is2dArray(array, message = null) { + if (this.stopIfNotActive_()) return; try { - if (typeof array === 'function') array = array(); - this.assert(Array.isArray(array) && Array.isArray(array[0]), message); - } catch(err) { + if ('function' === typeof array) array = array(); + let isArray = Array.isArray(array) && Array.isArray(array[0]); + if (message == null) message = "Input argument array is " + (isArray ? "2D array" : "not a 2D array"); + this.assert(isArray, message); + } catch (err) { + if (message == null) message = "Something was wrong"; this.assert(false, `${message}: ${err}`); } } + /** + * Logs out using header format (3 lines). It logs out to the console if attribute levelInfo >= 1. + * If other attributes such as enable, runningInGas indicate the test is not active no information is loged out. + */ printHeader(text) { - if(this.isInGas !== this.runningInGas) return; - console.log('*********************'); - console.log('* ' + text); - console.log('*********************'); + if (this.stopIfNotActive_()) return; + if (this.levelInfo >= 1) { + let len = ("string" === typeof text) ? text.length + 2 : 20; + if(len > 80) len = 80; + console.log("*".repeat(len)); + console.log('* ' + text) + console.log("*".repeat(len)); + } + } + + /** + * Logs out using sub header format (1 line). It logs out to the console if attribute levelInfo >= 1. + * If other attributes such as enable, runningInGas indicate the test is not active no information is loged out. + */ + printSubHeader(text) { + if (this.stopIfNotActive_()) return; + if (this.levelInfo >= 1) console.log('** ' + text); + } + + /** + * Logs out testing summary, If levelInfo is >= 1, then provides test statistics, informaing about total tests, + * number of failed tests and passed tests and in a second line summary line indicating all test passed if no test failed + * otherwise indicating some test failed. + * If levelInfo < 1, logs out only the content of the second line (summary line). + * If other attributes such as enable, runningInGas indicate the test is not active no information is loged out. + * @return {void} + */ + printSummary() { + if (this.stopIfNotActive_()) return; + let msg = "TOTAL TESTS=%d, ❌ FAILED=%d, ✔ PASSED=%d"; + if (this.levelInfo >= 1) console.log(Utilities.formatString(msg, _nTests.get(this), + _nFailTests.get(this), _nPassTests.get(this))); + console.log((_nFailTests.get(this) == 0) ? "ALL TESTS ✔ PASSED" : "❌ Some Tests FAILED"); } /** @@ -127,4 +278,4 @@ return UnitTestingApp; })(); -if (typeof module !== "undefined") module.exports = UnitTestingApp; \ No newline at end of file +if (typeof module !== "undefined") module.exports = UnitTestingApp;