This section will help you to deeply understand how to develop a Unit Test with Axional Test.

1 Introduction

Unit test programs are written in Javascript. Appart from standard Javascript libraries it can be used:

2 Getting started

The Axional Test module library is implemented by the test clause. It is both a function and a package. The function enables test unit definitions and the package implements extra test functionallities as it will be further seen.

Then, the test(name, fn) method allows run a test. The first argument is the test name; the second argument is a function that contains the expectations to test. For example, let's say there is a function fnMultiply() which receives two factors and returns the product. The whole test could be:

Copy
test("Eval 3x2=6", () => {
	const product = Ax.db.call("fnMultiply", 3, 2)
	Ax.jsunit.assertEquals(6, product);
});

Each instance of Axional Test Unit Test might content several tests. It allows evaluating critical characteristics within the same test implementation. For example:

Copy
test("Eval 3x2=6", () => {
	const product = Ax.db.call("fnMultiply", 3, 2)
	Ax.jsunit.assertEquals(6, product);
});

test("Eval 2.5x2=5", () => {
	const product = Ax.db.call("fnMultiply", 2.5, 2)
	Ax.jsunit.assertEquals(5, product);
});

test("Eval -3x2=-6", () => {
	const product = Ax.db.call("fnMultiply", -3, 2)
	Ax.jsunit.assertEquals(-6, product);
});

The previous case can be optimized by executing dynamic tests:

Copy
[
    [3, 2, 6],
    [2.5, 2, 5]
    [-3, 2, 6]
].forEach(testcase => {
    test(´Eval ${testcase[0]}x${testcase[1]}=${testcase[2]}´, () => {
	    const product = Ax.db.call("fnMultiply", ${testcase[0]}, ${testcase[1]})
	    Ax.jsunit.assertEquals(${testcase[2]}, product);
    });
});
It is remarkable that the execution of an Axional Test Unit test instance will produce one response for each execution of test(name, fn) method. For that reason, the second and third examples are equivalent.
 

3 Test response

The execution of an Axional Test Unit test instance produces one response for each execution of test(name, fn) method. So, one test implementation might produce more than one response. A test response can produce three different responses:

  • Succeed, when the execution is not interrupted by any assertion nor exception.
  • Failed, when the execution is interrupted by an unsuccessful assertion.
  • Error, when the execution is interrupted by an exception.

3.1 Successful response

A test execution is successful when is not interrupted by any assertion nor exception. The following code implements a sample of a test execution which produces a successful response:

Example
Copy
test('Succeed test', () => {
    Ax.jsunit.assertEquals(10, 10);
});

Result:

Name Result Message
Succeed test Succeed

3.2 Failure response

A test execution is failed when is interrupted by an unsuccessful assertion. The following code implements a sample of a test execution which produces a failure response:

Example
Copy
test('Failed test', () => {
    Ax.jsunit.assertEquals(10, 15);
});

Result:

Name Result Message
Failed test Failed Expected 10 (java.lang.Integer) while found 15 (java.lang.Integer)

3.3 Error response

A test execution is marked with error when is interrupted by an exception. The following code implements a sample of a test execution which produces an error response:

Example
Copy
test('Error test', () => {
    Ax.jsunit.assertEquals(10, amount);
});

Result:

Name Result Message
Error test Error Error: Undefined function or variable 'amount'

3.4 Test with multiple assertions

Each test unit might contain multiple assertions. The test is executed while the assertions found are successful. So, if a single assertion fails the unit test fails. The following code implements a sample of a test executing multiple assertion which produces a failure response:

Example
Copy
test('Multiple assertions', () => {
    Ax.jsunit.assertEquals(10, 10);
    
    Ax.jsunit.assertEquals(10, 15);
});

Result:

Name Result Message
Failed test Failed Expected 10 (java.lang.Integer) while found 15 (java.lang.Integer)

3.5 Multiple test executions

As it has been mentioned before, an instance of Axional Test Unit test can contain multiple test units. Every execution of a test unit produces a response, as it can be seen in the following example:

Example
Copy
test('Succeed test', () => {
    Ax.jsunit.assertEquals(10, 10);
});

test('Failed test', () => {
    Ax.jsunit.assertEquals(10, 15);
});

test('Error test', () => {
    Ax.jsunit.assertEquals(10, amount);
});

Result:

Name Result Message
Succeed test Succeed
Failed test Failed Expected 10 (java.lang.Integer) while found 15 (java.lang.Integer)
Error test Error Error: Undefined function or variable 'amount'

When having multiple unit test the global state of the test instance is the most severe. So, status severity sorted from the most severe to the lowest is: Error, Failure, Succeed. In consequence, the global state of the previous sample would be Error, even though it contains successful and failed tests.

4 Transaction handling

Tests are executed inside a transaction that is rolled back when all tests units have finished. This fact helps keeping test data consistent and prevents that one test execution influences other tests or future executions of the current test.

Example

Let's see an example of it:

Copy
test('Insert test', () => {
    Ax.db.insert("product", { name: "Water 500ml", price: 0.35 });
    
    const count = Ax.db.executeQuery("SELECT COUNT(*) count FROM product WHERE name = 'Water 500ml'").toOne().count;
    
    Ax.jsunit.assertEquals(1, count);
});

As it has been mentioned before, even the test success, after its execution the database will not have any product named 'Water 500ml'. Let's see the transaction flux of sample:

Loading...

Notice that it only exists one transaction for the whole test, so inner test units executions will be included in the same transaction.

Example

Let's see an example of it:

Copy
test('Insert test', () => {
    Ax.db.insert("product", { name: "Water 500ml", price: 0.35 });
    
    const count = Ax.db.executeQuery("SELECT COUNT(*) count FROM product WHERE name = 'Water 500ml'").toOne().count;
    
    Ax.jsunit.assertEquals(1, count);
});

test('Count test', () => {
    const count = Ax.db.executeQuery("SELECT COUNT(*) count FROM product WHERE name = 'Water 500ml'").toOne().count;
    
    Ax.jsunit.assertEquals(1, count);
});

In this case, both tests units will success, and after test execution the database will not have any product named 'Water 500ml' either. Let's see the transaction flux of sample:

Loading...

IMPORTANT

You should never use the commit function in a test when operating agaist the Master Test Data: an error will occur due to some functions.

5 Repeating Setup For Many Tests

When it is needed to do some work repeatedly for many tests, the test.beforeEach(fn) and test.afterEach(fn) methods can be used.

For example, in case of implementing a test for evaluating how triggers works in different situations. Let's say that several tests interact with a database of sales. There is an entity sale_head which contains the registry of each sale and an entity sale_line cointaining the items sold on each sale. The total amount of the sale is stored in the sale_head.amount and it is automatically updated via database triggers on each transaction of sale_line:

Copy
test.beforeEach(() => {
    Ax.db.insert("sale_head", { num: "O901", date: new Date() });
});

test.afterEach(() => {
    Ax.db.delete("sale_head", { num: "O901" });
});

test('Single insert', () => {
    Ax.db.insert("sale_line", { num: "O901", item: "Coke 200ml", qty: 1, price: 0.48 });
    
    const amount = Ax.db.executeQuery("SELECT amount FROM sale_head WHERE num = 'O901'").toOne();
    
    Ax.jsunit.assertEquals(0.48, amount);
});

test('Multiple inserts', () => {
    Ax.db.insert("sale_line", { num: "O901", item: "Coke 200ml", qty: 1, price: 0.48 });
    Ax.db.insert("sale_line", { num: "O901", item: "Mineral water 500ml", qty: 5, price: 0.35 });

    const amount = Ax.db.executeQuery("SELECT amount FROM sale_head WHERE num = 'O901'").toOne();
    
    Ax.jsunit.assertEquals(2.23, amount);
});

The graph bellow shows the test execution flux of the previous test sample:

Loading...

WARNING

test.beforeEach(fn) and test.afterEach(fn) do not cause any type of output, so no assertions should be made within these blocks.

6 One-Time Setup

In some cases, it is only need to do setup once, at the beginning or the end of the test execution. Axional Test provides test.beforeAll(fn) and test.afterAll(fn) to handle this situation.

WARNING

test.beforeAll(fn) and test.afterAll(fn) do not cause any type of output, so no assertions should be made within these blocks.

Recuperating the previous sample, but considering that the execution of the first test affects the second one, might lead to the following example:

Copy
test.beforeAll(() => {
    Ax.db.insert("sale_head", { num: "O901", date: new Date() });
});

test.afterAll(() => {
    Ax.db.delete("sale_head", { num: "O901" });
});

test('Eval insert', () => {
    Ax.db.insert("sale_line", { num: "O901", item: "Coke 200ml", qty: 1, price: 0.48 });
    
    const amount = Ax.db.executeQuery("SELECT amount FROM sale_head WHERE num = 'O901'").toOne();
    
    Ax.jsunit.assertEquals(0.48, amount);
});

test('Eval update', () => {
    Ax.db.update("sale_line", { qty: 2 }, { num: "O901", item: "Coke 200ml" } );
    
    const amount = Ax.db.executeQuery("SELECT amount FROM sale_head WHERE num = 'O901'").toOne();
    
    Ax.jsunit.assertEquals(0.96, amount);
});

The graph bellow shows the test execution flux of the previous test sample:

Loading...

Notice that the DELETE statement at the test.afterAll clause has been added for giving content to the sample explanation. But, as test execution do transaction rollback at the end of the test, it would not be necessary.

7 Assertions

Axional Test module uses "assertions" to let you test values in different ways. Each test unit is strongly recommended to have at least one assertion.

This section will introduce some commonly used assertions. For the full list, see the Axional JS Script Ax.junit API doc.

7.1 Common assertions

The simplest way to test a value is with exact equality:

  • Ax.jsunit.assertEquals, matches anything that an if statement treats two variables as equal
Copy
test('two plus two is four', () => {
    Ax.jsunit.assertEquals(4, 2 + 2);
});

Conversely, test that two values are different:

  • Ax.jsunit.assertNotEquals, matches anything that an if statement treats two variables as not equal
Copy
test('two plus two is not five', () => {
    Ax.jsunit.assertNotEquals(5, 2 + 2);
});

7.2 Truthiness

In tests sometimes is needed to distinguish between undefined, null, false, etc., and sometimes not. Axional JS Script Library contains helpers that let be explicit about that.

  • Ax.jsunit.assertTrue, matches anything that an if statement treats as true
  • Ax.jsunit.assertFalse, matches anything that an if statement treats as false
  • Ax.jsunit.assertEmpty, matches the values null, undefined, '', [] or {}
  • Ax.jsunit.assertNotEmpty, matches anything that an Ax.jsunit.assertEmpty statement treats as false
  • Ax.jsunit.assertNull, matches the values null or undefined
  • Ax.jsunit.assertNotNull, matches anything that an Ax.jsunit.assertNull statement treats as false

Some examples of use are:

Copy
test("null", () => {
        const n = null;
        Ax.jsunit.assertFalse(n);
        Ax.jsunit.assertEmpty(n);
});
Copy
test("zero", () => {
        const z = 0;
        Ax.jsunit.assertFalse(z);
        Ax.jsunit.assertNotEmpty(z);
});

7.3 Arrays and iterables

It can be checked if an array or iterable contains a particular item using indexOf and assertTrue assertion:

Copy
test("Array contains", () => {
        const a = ['Barcelona', 'London', 'Paris'];
        Ax.jsunit.assertTrue(a.indexOf('Barcelona') >= 0);
});

7.4 Dates

In programming languages dates handling is a common source of troubles and headaches. Generally, because depending on the programming language date types might be slightly different (consider time, calendar, etc.). Furthermore, in Axional Studio applications intervenes several programming languages in the same code (SQL, XSQL-Script, Javascript), thing that increases the problem.

For example, in case of implementing a test for evaluating a certain date value from a database of sales. There is an entity sale_head which contains a registry with the sales number 'O901' which its date of creation is May 21, 2018 23:15:05 (UTC). The date of creation is stored in the attribute date_created, with precision from year to second:

Copy
test("Date 'yyyy-mm-dd HH:mm:ss' verfication", () => {
    const date_created = Ax.db.executeQuery("SELECT date_created FROM sale_head WHERE num = 'O901'").toOne().date_created;
    Ax.jsunit.assertEquals(new Date("2018-05-21 23:15:05"), date_created);
});

Despite this, the most common dates verification in business applications does not consider time, but days. In order to easy those verifications it can be used the assertDateEquals assertion:

Bad use
Copy
test("Date 'yyyy-mm-dd' verfication", () => {
    const date_created = Ax.db.executeQuery("SELECT date_created FROM sale_head WHERE num = 'O901'").toOne().date_created;
    Ax.jsunit.assertEquals(new Date("2018-05-21"), date_created);
});

Notice that, the variable date_created contains the value of hours, minutes and seconds, and the value created by new Date("2018-05-21") not. in consequence, in that case the assertEquals execution will fail.

"2018-05-21 23:15:05" -> assertEquals -> "2018-05-21" = Test Failed

Good use
Copy
test("Date 'yyyy-mm-dd' verfication", () => {
    const date_created = Ax.db.executeQuery("SELECT date_created FROM sale_head WHERE num = 'O901'").toOne().date_created;
    Ax.jsunit.assertDateEquals(new Date("2018-05-21"), date_created);
});

"2018-05-21 23:15:05" -> assertDateEquals -> "2018-05-21" = Test Succeed

 

7.5 Snapshoting

Snapshot tests are a very useful tool when evaluating that complex data does not change unexpectedly. A snapshot is a "picture" of a piece of information at a certain moment. So, when execute assertions against a snapshot, it evaluates that the current snapshot is the same as the model snapshot. The model snapshot is the picture taken when test executes for first time. Consequently, the first time a snapshot test is executed will never fail, so it will only store the taken snapshot as model.

WARNING

When a snapshot model is created for the first time or after performing a reset, it is essential to verify the validity of its content. An incorrect snapshot model will invalidate any test result.

A typical snapshot test case for an accounting app generates an XML structure with the accounting data of a period, takes a snapshot, then compares it to a reference snapshot model stored alongside the test. The test will fail if the two snapshots do not match: either the change is unexpected, or the reference snapshot needs to be updated to the new version of the XML structure.

In order to execute simple assertions against a snapshot, use assertEquals.

Copy
test("Model 303 for 2018", (test_unit) => {
    const model_303 = Ax.db.call("accounting_calculate_303", { period_from: new Date("2018-01-01"), period_to: new Date("2018-12-31") })
    Ax.jsunit.assertEquals(test_unit.snapshot, model_303);
});

It is important to note that the test function receives the param test_unit, which is used to reference the snapshot for the current test unit. In consequence, each test unit can only contain a single snapshot. In case of require more than one the unit must be split in several tests.

Bad use

Copy
test("Model 303 for 2017 and 2018", (test_unit) => {
    const model_303_2017 = Ax.db.call("accounting_calculate_303", { period_from: new Date("2017-01-01"), period_to: new Date("2017-12-31") })
    Ax.jsunit.assertEquals(test_unit.snapshot, model_303_2017);

    const model_303_2018 = Ax.db.call("accounting_calculate_303", { period_from: new Date("2018-01-01"), period_to: new Date("2018-12-31") })
    Ax.jsunit.assertEquals(test_unit.snapshot, model_303_2018);
});

Good use

Copy
test("Model 303 for 2017", (test_unit) => {
    const model_303 = Ax.db.call("accounting_calculate_303", { period_from: new Date("2017-01-01"), period_to: new Date("2017-12-31") })
    Ax.jsunit.assertEquals(test_unit.snapshot, model_303);
});

test("Model 303 for 2018", (test_unit) => {
    const model_303 = Ax.db.call("accounting_calculate_303", { period_from: new Date("2018-01-01"), period_to: new Date("2018-12-31") })
    Ax.jsunit.assertEquals(test_unit.snapshot, model_303);
});

7.5.1 Snapshot read

When the test is run for the first time, the Model snapshot is registered. From the Execution results area in the Test Definition form it is possible to unload the Model Snapshot file by means the diskette icon (blue color when available).

The subsequent times, test result will be registered in the Snapshot Execution file, comparing this file with the model file (both are excel files). Remember that when a Test_Unit has been divided into several tests, also several responses to each snapshot will be obtained.

  • When both snapshot files match, the response will be Succeed.
  • If the response is Failed, the application displays a short message about the failure. To make an intensive comparison, this file can also be downloaded using the blue diskette.

7.5.2 Updating snapshots

When the Snapshot model is no longer valid, it will be necessary to update the Model file. To that end, access the Test Definition / Unit test and select desired Test:

  • Use the Reset button to delete snapshot files: this will erase both files, model and execution files. In fact, it will erase the entire row, including all counters, user, date, etc...
  • Execute the test: this test result will be recorded and it will become the new model until next reset.

7.6 Business data

Axional Test module is usually used for evaluating business data. Generally, those evaluations include complex information and a huge amount of attributes which becomes difficult to analize. Let's see an example:

Copy
test("Test sales triggers", (test_unit) => {
    Ax.db.insert("sale_head", { num: "O901", date: new Date() });
    Ax.db.insert("sale_line", { num: "O901", item: "Coke 200ml", qty: 1, price: 0.48 });
    Ax.db.insert("sale_line", { num: "O901", item: "Mineral water 500ml", qty: 5, price: 0.35 });
    
    const head_amount = Ax.db.executeQuery("SELECT amount FROM sale_head WHERE num = 'O901'").toOne();
    Ax.junit.assertEquals(2.23, head_amount);
    
    const rsSaleLines = Ax.db.executeQuery("SELECT amount, item FROM sale_line WHERE num = 'O901'");
    
    // Evaluate first line is correct
    Ax.jsunit.assertTrue(rsSaleLines.next());
    Ax.junit.assertEquals("Coke 200ml", rsSaleLines.toRow().item);
    Ax.junit.assertEquals(0.48, rsSaleLines.toRow().amount);
    
    // Evaluate second line is correct
    Ax.jsunit.assertTrue(rsSaleLines.next());
    Ax.junit.assertEquals("Mineral water 500ml", rsSaleLines.toRow().item);
    Ax.junit.assertEquals(1.75, rsSaleLines.toRow().amount);
    
    // Evaluate the are no more lines
    Ax.jsunit.assertFalse(rsSaleLines.next());
    
    rsSaleLines.close();
});

The previous code is completely correct, but it might be quite complicated and it could even become more tangled as more data is added to verify. In order to test that a particular verifications, use assertResultSet.

Copy
test("Test sales triggers with resultSet assertion", (test_unit) => {
    Ax.db.insert("sale_head", { num: "O901", date: new Date() });
    Ax.db.insert("sale_line", { num: "O901", item: "Coke 200ml", qty: 1, price: 0.48 });
    Ax.db.insert("sale_line", { num: "O901", item: "Mineral water 500ml", qty: 5, price: 0.35 });
            
    Ax.jsunit.assertResultSet(test_unit.snapshot, [
        {
            tableName: "sale_head",
            columns  : "num,date,amount",
            where    : "num = '0901'"
        },
        {
            tableName: "sale_line",
            columns  : "item,qty,price,amount",
            where    : "num = '0901'"
        }
    ]);
});
Assertions performed by assertResultSet must be always executed against an snapshot.

7.7 Exceptions

In order to test that a particular function throws an error when it's called, use assertThrows.

Copy
test("Call throws exception", () => {
        Ax.jsunit.assertThrows("Cannot delete invoice because is already accounted", () => {
            Ax.db.delete("invoice", { num: "I2019-00001" });
        });
});