Blog-Archiv

Dienstag, 1. Mai 2018

Getting Used to TypeScript by Testing

In this Blog I will describe how I tested my recent TypeScript implementations.

My problem is that I want to stick to EcmaScript 6 import statements, but nodejs seems to not support them. So I decided to create a small test framework based on the browser's JS engine. At least the web-browser is the final goal of all TypeScript or JavaScript code.

Infrastructure

This is not an infrastructure for professional TypeScript projects!

I have a number of source directories, nested into each other. In case you compile with tsc -t ES6, the TS source files there do not see each other unless you use import statements, even when they are in the same directory. Following outlines one of these directories, it is the one that contains the sources for Point, Dimension, Rectangle and their implementation classes. These are the targets of my test.

ts-examples
test.html
interfaces
geometry
Dimension.ts
DimensionImpl.ts
Point.ts
PointImpl.ts
Rectangle.ts
RectangleImpl.ts
test.ts

The page test.html is my test page, and interfaces/geometry/test.ts is the TypeScript test I want to execute. Because test.html is above all of my source directories, I can run many test.ts with it, I just need to write their relative paths into test.html. See below how this works.

Before I run the test by loading test.html into an ES6-able web-browser, I need to compile my TS sources with:

cd ts-examples/interfaces/geometry
tsc -t ES6 *.ts

This leaves an XXX.js file for each XXX.ts file in the same directory.

Test Page HTML

Here comes the test.html page.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8"/>
        <title>TypeScript Test Page</title>
    </head>

    <body>
        <div style="position: fixed; left: 4px; right: 4px; background-color: lightGray; padding: 4px;">
            Number of tests: <span id="numberOfTests">0</span>, fails: <span id="numberOfFails">0</span>
        </div>
        
        <div id="logPanel" style="padding-top: 2em;"></div>
        
        <script>
            var increment = function(elementId) {
                var number = document.getElementById(elementId);
                number.innerHTML = ""+(parseInt(number.innerHTML) + 1);
            };
            
            var logPanel = document.getElementById("logPanel");
            var consoleLog = console.log;
            
            console.log = function(output, color, isTitle) {
                if ( ! isTitle && consoleLog )
                    consoleLog(output);
                    
                var line = document.createElement(isTitle ? "H2" : "DIV");
                line.style.color = (color !== undefined) ? color : "green";
                line.innerHTML = output;
                logPanel.appendChild(line);
                
                if ( isTitle )
                    return;
                    
                increment("numberOfTests");
                if (color === "red")
                    increment("numberOfFails");
            };
            
            window.addEventListener("error", function(error) {
                if ( ! error.error )
                    return;
                    
                console.log(error.error, "magenta");
            });
        </script>
        
        <script>
            var title = function(testTitle) {
                console.log(testTitle, "blue", true);
            };
            
            var assert = function(criterion, message) {
                if (criterion)
                    console.log(message);
                else
                    console.log(message, "red");
            };
        </script>

        
        <!-- Tests to display on this page -->
        
        <script type="module" src="interfaces/geometry/test.js"></script>
        <!-- <script type="module" src="interfaces/indexable-types/test.js"></script> -->
        <!-- <script type="module" src="interfaces/interfaces-are-open-ended/test.js"></script> -->
        
    </body>

</html>

Line 9 - 11 represent a fixed top-bar that shows how many tests were executed, and how many of them failed.

Line 13 contains the output panel that shows all titles and messages from the executed tests. In case the test fails, the message is displayed in red, in case of success green.

Line 15 - 47 hold a JavaScript that manages these outputs.
The increment() function will read a number from an element, increment it, and store it back to the element. To get all outputs into the page, the console.log function is overridden by a function that writes into the HTML page. It allows three parameters, additionally to the output text also a color for the output, and a flag whether the output is a test-title. This anonymous function writes into the log area and increments the counters on top.
Finally a page error handler is installed via window.addEventListener("error", ....), this redirects any possible JS interpreter runtime exception also to the page, the color would be magenta.

Line 49 - 60 provide two utility functions for tests: The ubiquitous assert(), and a possibility for the test to state its title(). The color of a title will be blue, an error will be red, any other message will have the default color green. These two functions must be declared by any test.js file. See below how to do that.

Line 65 - 67 contain the test scripts to execute. Currently just one is commented-in, so we will see just one title.

Test Script TS

Finally here is the TS code that tests the sources in same directory. Mind that in a real project you would try to separate tests from production code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import { Point } from "./Point.js";
import { PointImpl } from "./PointImpl.js";
import { Dimension } from "./Dimension.js";
import { DimensionImpl } from "./DimensionImpl.js";
import { Rectangle } from "./Rectangle.js";
import { RectangleImpl } from "./RectangleImpl.js";

declare function title(assertTitle: string): void;
declare function assert(criterion: boolean, message: string): void;

title("Geometry");

// Point

const point: Point = new PointImpl(3, 4);
assert(
    point.distance() === 5,
    "Point(3, 4) distance from origin(0, 0) must be 5!");
    
assert(
    point.distance(new PointImpl(-3, -4)) === 10, 
    "Point(3, 4) distance from point(-3, -4) must be 10!");
    
const transformedPoint: Point = point.transform(new PointImpl(7, 10));
assert(
    transformedPoint.x === -4 && transformedPoint.y === -6,
    "Point(3, 4) transformation to origin(7, 10) must be (-4, -6)!");
    
// Dimension

const dimension: Dimension = new DimensionImpl(2, 3);
assert(
    dimension.area() === 6,
    "Dimension(2, 3) area must be 6!");

// Rectangle

const rectangle: Rectangle = new RectangleImpl(point, dimension);
assert(
    rectangle.x === point.x && point.x === 3 && rectangle.y === point.y && point.y === 4,
    "Rectangle location must be ("+point.x+", "+point.y+")!");
assert(
    rectangle.width === dimension.width && rectangle.height === dimension.height,
    "Rectangle dimension must be ("+dimension.width+", "+dimension.height+")!");
assert(
    rectangle.area() === dimension.area(),
    "Rectangle area must be "+dimension.area()+"!");

const movedRectangle = rectangle.move(transformedPoint);
assert(
    movedRectangle.x === transformedPoint.x && movedRectangle.y === transformedPoint.y,
    "Rectangle must have moved to ("+transformedPoint.x+", "+transformedPoint.y+")!");
    
const newDimension: Dimension = new DimensionImpl(4, 7);
assert(
    rectangle.resize(newDimension).area() === 28,
    "Rectangle must have been resized to an area of 28!");

//rectangle.width = 999; // compile error!

Unlike in Java there is no heading package statement in TS. The source starts with importing classes and interfaces it wants to test.

Line 8 - 9 contain the declaration of external functions that test.html will provide. Without this the compiler would report an error.

The TS declare keyword is like the C extern, or the Java abstract, it lets the compiler know that there will be something in place at runtime, and it defines the type of it, so that the compilation does not fail.

Line 11 outputs the title of the test, to be seen in blue.

The remaining lines contain calls to objects of the imported classes, and subsequent assertions about the result of these calls. Each assert() call will generate a red or green line in the HTML page.

The out-commented statement in Line 59 tries to set a value into the readonly property width. I had to comment it out because the compiler reported an error. If you would put it into the compiled test.js, it would work, the property would actually accept the value. All these checks that TS provides exist just at compile time!

This is a screenshot of test.html after loading it via a file-URL. (I tweaked one of the tests to fail for trying out if it would be displayed red.)

Conclusion

This Blog showed how TS can be tested directly in a browser, using just one HTML-page with a small JS framework.

After having spent a lot of time trying to make nodejs execute my compile results that use ES6 import statements, I am glad that I can test my TS code in that easy way now.




Keine Kommentare: