Wednesday, September 7, 2016

JavaScript basic testing with chai and mocha

For learning testing process in JS, I decided to create a simple application and write test for every step. For tests I will use libraries mocha and chai.


1. Goal.

My aplication(script) will be very simple: I just want to create a solution for finding the way from PointA to PointB on the map. Map is an 2 dimensional array marked with 1 - we can enter this point, -0 - can't:


Result of execution should be array of steps: [4,4]<-[4,3]<-[3,3]<-[2,3]<-[2,2]<-[2,1]<-[2,0]<-[1,0]<-[0,0]
During implementation I will be testing every step just after creation,


2. Preparation.

First of all we need several files:
- html file for displaying test results: test.html
- our script file with algorythm implementation: MyScript.js
- file with test scripts for previous step: MyScriptTest.js
- libraries mocha.js and chai.js - they will be used as external scripts.
All this files I put to one directory. Here is the content of test result file: test.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">

  <!-- Mocha css -->  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.1.0/mocha.css">
  <!-- Mocha dependency -->  <script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.1.0/mocha.js"></script>
  <!-- Mocha: setup BDD -->  <script> mocha.setup('bdd'); </script>
  <!-- chai dependency -->  <script src="https://cdnjs.cloudflare.com/ajax/libs/chai/2.0.0/chai.js"></script>
  <!-- export assert -->  <script>  var assert = chai.assert;  </script>
</head>

<body>
  <!-- script which should be tested -->  <script src="MyScript.js"></script>
  <!-- test itself -->  <script src="MyScriptTest.js"></script>
  <!-- element with id="mocha" for test results -->  <div id="mocha"></div>
  <!-- run tests! -->  <script>
    mocha.run();
  </script>
</body>
</html>

As you can see, mocha and chai libraries will be downloaded by SRIPT tag. Also we set up mocha for using BDD, exported "assert" for simple usage and executed mocha test running.
In addition, we need just 2 files:
<!-- script which should be tested --><script src="MyScript.js"></script>
<!-- test itself --><script src="MyScriptTest.js"></script>

- we will be creating them step-by-step during implementation. First - function for MyScript, next - testblock for that function in MyScriptTest.js.

3.Implementation

For tests(content of file MyScriptTest.js) I will be using next 2 arrays, one for "map", second - "work" array for marking which points were visited:
var map;
var visited;

beforeEach(function () {
    map = [
        [1, 0, 0, 0, 0],
        [1, 0, 1, 1, 0],
        [1, 1, 1, 1, 0],
        [1, 0, 0, 1, 0],
        [1, 0, 0, 1, 1]
    ];
    visited = [
        [0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0]
    ];
});


"beforeEach" will set up this arrays before every test.


3.1 Point class(content of file MyScript.js)

First of all I created class for more simple operations X and Y coordinates:
function Point(x, y, parent) {
    var self = Object.create(null);
    self.x = x;
    self.y = y;
    self.parent = parent;
    self.level = 0;
    if (parent) {
        self.level = parent.level + 1;
    }
    self.equals = function (anotherPoint) {
        return (self.x === anotherPoint.x && self.y === anotherPoint.y);
    };
    self.toString = function () {
        return "[" + self.x + "," + self.y + "]";
    };
    return self;
}

Next, let's test it(content of file MyScriptTest.js):
describe("Point class", function () {
    it("should return TRUE on calling EQUALS for another point with the same coordinates", function () {
        var point = new Point(1, 2);
        var theSamePoint = new Point(1, 2);
        assert.equal(point.equals(theSamePoint), true);
    });

    it("should return FALSE on calling EQUALS for another point with the different coordinates", function () {
        var point = new Point(1, 2);
        var anotherPoint = new Point(1, 3);
        assert.equal(point.equals(anotherPoint), false);
    });

    it("should return LEVEL=0 if parent is NULL", function () {
        var point = new Point(1, 2);
        assert.equal(point.level, 0);
    });

    it("should have LEVEL=parent.LEVEL+1 if parent is NOT NULL", function () {
        var point = new Point(1, 2);
        var childPoint = new Point(2, 3, point);
        var nextChild = new Point(3, 4, childPoint);
        assert.equal(childPoint.level, 1);
        assert.equal(nextChild.level, 2);
    });

});



3.2 Function for position validation.


Function code(content of file MyScript.js):
function isValidPosition(point, map) {
    var x = point.x;
    var y = point.y;
    //logger.trace("    checking isValidPosition: " + point.toString());    return (x >= 0 && x < Object.keys(map).length && y >= 0 && y < map[0].length);
}

Test(content of file MyScriptTest.js):
describe("isValidPosition", function () {
    it("should be TRUE for coordinates inside map", function () {
        assert.equal(isValidPosition(new Point(1, 1), map), true);
    });

    it("should be FALSE for coordinates with x<0", function () {
        assert.equal(isValidPosition(new Point(-1, 1), map), false);
    });

    it("should be FALSE for coordinates with x>=map.length", function () {
        assert.equal(isValidPosition(new Point(Object.keys(map).length, 1), map), false);
    });

    it("should be FALSE for coordinates with y<0", function () {
        assert.equal(isValidPosition(new Point(1, -1), map), false);
    });

    it("should be FALSE for coordinates with y>=map.length", function () {
        assert.equal(isValidPosition(new Point(1, map[0].length), map), false);
    });

});


3.3. Function for finding points around current point(to which point we can go from current point).

Function code(content of file MyScript.js):
function getPointsAround(point) {
    var x = point.x;
    var y = point.y;
    return [new Point(x - 1, y, point), new Point(x + 1, y, point), new Point(x, y - 1, point), new Point(x, y + 1, point)];
}

Test(content of file MyScriptTest.js):
describe("getPointsAround", function () {
    var x = 1;
    var y = 1;
    var point = new Point(x, y);
    var positions = getPointsAround(point);

    it("Should return 4 positions around point", function () {
        assert.equal(positions.length, 4);
    });

    it("Should be [x-1,y] for position #1", function () {
        assert.equal(positions[0].equals(new Point(x - 1, y)), true);
    });

    it("Should be [x+1,y] for position #2 ", function () {
        assert.equal(positions[1].equals(new Point(x + 1, y)), true);
    });

    it("Should be [x,y-1] for position #3 ", function () {
        assert.equal(positions[2].equals(new Point(x, y - 1)), true);
    });

    it("Should be [x,y+1] for position #4 ", function () {
        assert.equal(positions[3].equals(new Point(x, y + 1)), true);
    });

});



3.4. Function getNextPoints - combination of 2 previous steps: we are taking all points around and checking if they are valid and not visited.

Function code of 2 additional check functions (content of file MyScript.js):
function isVisited(point, visited) {
    //logger.trace('    checking isVisited: [' + point.x + ',' + point.y + ']');    var p = visited[point.x][point.y];
    if (typeof p == "object") {
        return true;
    } else {
        return p == 1;
    }
}

function isAcessable(point, map) {
    //logger.trace("    checking isAcessable: " + point.toString());    return map[point.x][point.y] == 1;
}


Function code of getNextPoints(content of file MyScript.js):
function getNextPoints(point, map, visited) {
    //logger.trace("  getNextPoints: " + point.toString());    var nexts = getPointsAround(point);
    var result = [];
    nexts.map(function (next) {
        if (isValidPosition(next, map) && isAcessable(next, map) && !isVisited(next, visited)) {
            result.push(next);
        }
    });
    return result;
}



Test(content of file MyScriptTest.js):
describe("getNextPoints", function () {
    var nexts;
    it("Should return 1 next point from position [0,0]", function () {
        nexts = getNextPoints(new Point(0, 0), map, visited);
        assert.equal(nexts.length, 1);
        assert.equal(nexts[0].equals(new Point(1, 0)), true);
    });

    it("Should return 2 next points from position [2,1]", function () {
        nexts = getNextPoints(new Point(2, 1), map, visited);
        assert.equal(nexts.length, 2);
        assert.equal(nexts[0].equals(new Point(2, 0)), true);
        assert.equal(nexts[1].equals(new Point(2, 2)), true);
    });

    it("Should return 1 next point from position [2,1] if position [2,0] was visited", function () {
        visited[2][0] = 1;
        nexts = getNextPoints(new Point(2, 1), map, visited);
        assert.equal(nexts.length, 1);
        assert.equal(nexts[0].equals(new Point(2, 2)), true);
    });
});

3.5. Main function with startPoint, endPoint and map.

Also it use additional function for extraction of full path from endPoint (content of file MyScript.js):
function extractFullPath(point) {
    var result = [];
    var path = point.toString();
    while (point.parent) {
        result.push(point);
        point = point.parent;
        path = path + "<-"+point.toString();
    }
    result.push(point);
    logger.trace(path);
    return result;
}


Function code(content of file MyScript.js):
function go(startPoint, endPoint, map, visited) {
    var queue = [];
    var current;
    var nexts;

    queue.push(startPoint);
    while (queue.length > 0) {
        current = queue.shift();
        //logger.trace('processing from queue: [' + current.x + ',' + current.y + ']');        if (current.equals(endPoint)) {
            return extractFullPath(current);
        }
        visited[current.x][current.y] = current;

        nexts = getNextPoints(current, map, visited);

        nexts.map(function (next) {
            queue.push(next);
        });
    }
}



Test(content of file MyScriptTest.js):
describe("go", function () {
    var startPoint = new Point(0, 0);
    var endPoint = new Point(4, 4);

    it("Should return valid path from startPoint to endPoint for valid set of parameters", function () {
        var result = go(startPoint, endPoint, map, visited);
        assert.equal(result[0].equals(endPoint), true);
        assert.equal(result[result.length - 1].equals(startPoint), true);
    });

});

Final test is checking if we really have a correct result path: from startPoint to endPoint.

3.6 Test results.

As a result we have a user friendly report with test list. By clicking on test we can see test details(on screenshot result after clicking of getPointsAround for position#1)


Also string version of result with the exact path was printed to console: [4,4]<-[4,3]<-[3,3]<-[2,3]<-[2,2]<-[2,1]<-[2,0]<-[1,0]<-[0,0].

4. The End. 

I found testing with mocha and chai is very simple and powerful. Asserts with chai looks for me very similar to JUnit, writing BDD test with mocha - to groove spock library. Archive with all source files can be downloaded from here

No comments:

Post a Comment