Friday, October 14, 2011

Integrating Javascript tests into a CLI build

Wherein we walk through a basic setup for running Javascript unit tests on the command line. After some initial investigation (here) I didn't find time to get back to Javascript unit testing until recently. I have now managed to get Javascript unit tests running fairly gracefully in a command line build at work; here is an outline of how, simplified from the "real" implementation to highlight the basics. Fortunately a great deal of the work is done for us, always nice when it turns out that way.

We are going to run everything off the filesystem to avoid having our tests impacted by external influences.

Part 1: Basic test setup
  1. Create a directory to house Javascript unit test files; we will refer to this as \jsunit henceforth when giving paths. 
  2. Download QUnit.js and QUnit.css into \jsunit
  3. Download run-qunit.js into \jsunit
  4. Create a file testme.js in \jsunit with the following content
    /**
     * var-args; adds up all arguments and returns sum
     */
    function add() {
    }
    
  5. Create a file testme.test.htm in \jsunit with the following content
    • Note we are using local filesystem paths to load all content; we have no external dependencies
    • <!DOCTYPE html>
      <html>
      <head>
      	<!-- we need QUnit as a test runner -->
          <link rel="stylesheet" href="qunit.css" type="text/css" media="screen" />
          <script src="qunit.js"></script>
      	
      	<!-- we'd like to have the file we're going to test -->
          <script src="testme.js"></script>
      	
      	<!-- and finally lets write some tests -->
      	<script>
      		console.log("test time baby");
      
      		test("add is defined", function() {
      			equals(typeof window.add, "function", "add isn't a function :(");
      		});
      	</script>
          
      </head>
      <body>
      	 <h1 id="qunit-header">QUnit Tests</h1>
      	 <h2 id="qunit-banner"></h2>
      	 <div id="qunit-testrunner-toolbar"></div>
      	 <h2 id="qunit-userAgent"></h2>
      	 <ol id="qunit-tests"></ol>
      	 <div id="qunit-fixture"></div>    
      </body>
      </html>
      
  6. Download PhantomJS (1.3.0 at time of writing)
    • For example PhantomJS commands I will assume it is on PATH (eg phantomjs args); use the qualified path if not (eg C:\where\phantom\is\phantomjs args)
  7. Open testme.test.htm in a browser; it should look like this:

  8. Open a command prompt, navigate to \jsunit and run phantomjs run-qunit.js testme.test.htm
    • Output should be similar to:
      test time baby
      'waitFor()' finished in 211ms.
      Tests completed in 57 milliseconds.
      1 tests of 1 passed, 0 failed.
      
    • Note we don't see any "test blah pass" or "test two fail" style output
Part 2: CLI build integration prep
So far so good, now we need to get setup to run in a CLI build. There a couple of things we'd like here, most of which are already implemented in run-qunit.js:
  1. Output each test pass/fail
  2. Output log messages from tests to the console
    1. This "just works" courtesy of run-qunit.js, yay!
  3. Exit with non-zero error code if tests fail
    1. This makes it easy for build to detect failure and do something in response; for example an Ant build could simply set failonerror
    2. This "just works" courtesy of run-qunit.js, yay!
We just have to setup output of test pass/fail information to the console. We'll add a test that fails to show what that looks like. Proceed as follows:
  1. Create a file test-support.js in \jsunit with the following content:
    //create a scope so we don't pollute global
    (function() {  
       var testName;
       
       //arg: { name }
    	QUnit.testStart = function(t) {
    	    testName = t.name;
    	};
    	
    	//arg: { name, failed, passed, total }
    	QUnit.testDone = function(t) {
    	    console.log('Test "' + t.name + '" completed: ' + (0 === t.failed ? 'pass' : 'FAIL'))
    	};
    	
    	//{ result, actual, expected, message }
    	QUnit.log = function(t) {
    	    if (!t.result) {
    	        console.log('Test "' + testName + '" assertion failed. Expected <' + t.expected + '> Actual <' + t.actual + '>' + (t.message ? ': \'' + t.message + '\'' : ''));
    	    }
    	};
    }());
    
  2. Edit testme.test.htm to pull in test-support.js and add a test that will currently fail
    • <!DOCTYPE html>
      <html>
      <head>
      	<!-- we need QUnit as a test runner -->
          <link rel="stylesheet" href="qunit.css" type="text/css" media="screen" />
          <script src="qunit.js"></script>
      	
      	<!-- where would our tests be without support! -->
      	<script src="test-support.js"></script>
      	
      	<!-- we'd like to have the file we're going to test -->
          <script src="testme.js"></script>
      	
      	<!-- and finally lets write some tests -->
      	<script>
      	
      		test("add is defined", function() {
      			equals(typeof window.add, "function", "add isn't a function :(");
      		});
      		
      		test("add 1+1", function() {
      			equals(add(1, 1), 2);
      		});		
      	</script>
          
      </head>
      <body>
      	 <h1 id="qunit-header">QUnit Tests</h1>
      	 <h2 id="qunit-banner"></h2>
      	 <div id="qunit-testrunner-toolbar"></div>
      	 <h2 id="qunit-userAgent"></h2>
      	 <ol id="qunit-tests"></ol>
      	 <div id="qunit-fixture"></div>    
      </body>
      </html>
      
  3. Open a command prompt, navigate to \jsunit and run phantomjs run-qunit.js testme.test.htm
    • Output should be similar to:
      Test "add is defined" completed: pass
      Test "add 1+1" assertion failed. Expected <2> Actual <undefined>
      Test "add 1+1" completed: FAIL
      'waitFor()' finished in 209ms.
      Tests completed in 70 milliseconds.
      1 tests of 2 passed, 1 failed.
    • If you print the exit code (echo %ERRORLEVEL% in Windoze) you should get a 1, indicating we have fulfilled the 'exit with non-zero exit code on failure' requirement :)
Part 3: Ant integration
At long last we are ready to integrate this mess into a build. For this example I will use Ant and will assume Ant is on PATH. At time of writing I am using Ant 1.8.2.

  1. Create a phantomjs.bat file in \jsunit with the following content
    @echo off
    C:\where\you\put\phantom\phantomjs.exe %*
    
    • Alternately create phantomjs.sh with equivalent functionality if on *nix
  2. Create a build.xml file in \jsunit with the following content
    • <?xml version="1.0" encoding="UTF-8"?>
      <project name="jsunittests" basedir="." default="main">
      	<property name="builddir" location="${basedir}/target"/>
      	
      	<condition property="phantom.filename" value="phantomjs.bat"><os family="windows"/></condition>
      	<condition property="phantom.filename" value="phantomjs.sh"><os family="unix"/></condition>   
      	
      	<target name="clean">
      		<delete dir="${builddir}"/>
      	</target>
      	
      	<target name="prep">
      		<mkdir dir="${builddir}"/>
      	</target>
      	
      	<target name="jstest">
            <!--Run all tests w/phantom, fail if tests fail. Execute all files w/extension .test.htm. -->
            <apply executable="${phantom.filename}" failonerror="true" dir="${basedir}" relative="true">
               <arg value="run-qunit.js"/>
               <fileset dir="${basedir}">
                  <include name="**/*.test.htm" />
               </fileset>
            </apply>			
      	</target>
      	
      	<target name="main" depends="clean, prep, jstest">
      	</target>
      </project>
      
  3. Run 'ant'; you should get output similar to the following (yes, it's supposed to fail, remember we have a test that fails setup on purpose)
    • Buildfile: build.xml
      
      clean:
         [delete] Deleting directory C:\Code\jsunit-trial\target
      
      prep:
          [mkdir] Created dir: C:\Code\jsunit-trial\target
      
      jstest:
          [apply] Test "add is defined" completed: pass
          [apply] Test "add 1+1" assertion failed. Expected <2> Actual 
          [apply] Test "add 1+1" completed: FAIL
          [apply] 'waitFor()' finished in 218ms.
          [apply] Tests completed in 58 milliseconds.
          [apply] 1 tests of 2 passed, 1 failed.
      
      BUILD FAILED
      C:\Code\jsunit-trial\build.xml:18: apply returned: 1
      
      Total time: 0 seconds
      
  4. Edit testme.js just enough to fix the test
    • /**
       * var-args; adds up all arguments and returns sum
       */
      function add() {
      	var sum =0;
      	for (var i=0; i<arguments.length; i++)
      		sum += arguments[i];
      	return sum;
      }
  5. Run 'ant'; you should get output similar to the following
    • Buildfile: build.xml
      
      clean:
         [delete] Deleting directory C:\Code\jsunit-trial\target
      
      prep:
          [mkdir] Created dir: C:\Code\jsunit-trial\target
      
      jstest:
          [apply] Test "add is defined" completed: pass
          [apply] Test "add 1+1" completed: pass
          [apply] 'waitFor()' finished in 214ms.
          [apply] Tests completed in 59 milliseconds.
          [apply] 2 tests of 2 passed, 0 failed.
      
      main:
      
      BUILD SUCCESSFUL
      Total time: 0 seconds
Pretty sweet, we've got Javascript tests running in an Ant build as a first-class citizen. Now if you break my Javascript my Continuous Integration server will let me know!

Part 4: Code coverage
Finally we are ready to get some code coverage. We are going to get code coverage by instrumenting our js files using JSCoverage, running our QUnit tests such that the relative paths resolve to the instrumented copies, and then using the PhantomJS file system APIs to create a colorized copy of the original js file to visually display coverage. We'll do a quick and dirty percentage coverage output to the console as well.


  1. Download JSCoverage 0.5.1
  2. Create a jscoverage.bat file in \jsunit with the following content
    @echo off
    C:\where\you\put\jscoverage\jscoverage.exe %*
    
  3. Create a template file for coverage information named coverageBase.htm in \jsunit
    • <!DOCTYPE html>
      <html>
      <head>
          <style>
              .code {
                  white-space: pre;
                  font-family: courier new;
                  width: 100%;            
              }
              
              .miss {
                  background-color: #FF0000;
              }
              
              .hit {
                  background-color: #94FF7C;
              }
              
              .undef {
                  background-color: #AFFF9E;
              }        
          </style>
      </head>
      <body>
      
      COLORIZED_LINE_HTML
      
      </body>
      </html>
      
  4. Update build.xml to perform a few new steps
    1. Create a \target\testjs\js directory and copy our js files into it
    2. Index our js files for code coverage, putting the indexed version into \target\testjs\jsinstrumented
    3. Copy *.test.htm into \target\testhtm
    4. Copy base resources to run tests (run-qunit.js, qunit.js, qunit.css) into \target\testhtm
    5. Copy the instrumented js files into \target\testhtm
      1. Note that because we used relative paths to our test js files the *.test.htm QUnit html files will now resolve js to the instrumented version when we run the files out of \target\testhtm
    6. Run PhantomJS on *.test.htm in \target\testhtm
    7. The updated build.xml looks like this:
    8. <?xml version="1.0" encoding="UTF-8"?>
      <project name="jsunittests" basedir="." default="main">
      	<property name="builddir" location="${basedir}/target"/>
      	<property name="jstestdir" location="${builddir}/testjs"/>
      	<property name="jsdir" location="${jstestdir}/js"/>
      	<property name="jsinstrumenteddir" location="${jstestdir}/jsinstrumented"/>
      	<property name="testhtmdir" location="${builddir}/testhtm"/>
      	
      	<condition property="phantom.filename" value="phantomjs.bat"><os family="windows"/></condition>
      	<condition property="phantom.filename" value="phantomjs.sh"><os family="unix"/></condition>   
      	
      	<property name="jscoverage.filename" value="jscoverage.bat" />
      	
      	<target name="clean">
      		<delete dir="${builddir}"/>
      	</target>
      	
      	<target name="prep">
      		<mkdir dir="${jsdir}"/>
      		<mkdir dir="${jsinstrumenteddir}"/>		
      		<mkdir dir="${testhtmdir}"/>
      		
      		<!-- copy non-test js files to target so we can mess with 'em. how we select which files may vary; for this 
      			 example just pick the one file we are testing.-->
      		<copy todir="${jsdir}">
      			<fileset dir="${basedir}">
      				<include name="testme.js" />
      			</fileset>
      		</copy>
      				
      		<!-- run jscoverage to produce a version of the file instrumented for code coverage -->
      		<exec executable="${jscoverage.filename}" failonerror="true">
      			<arg value="${jsdir}"/>
      			<arg value="${jsinstrumenteddir}"/>
      		</exec>   		
      		
      		<!-- copy our test htm files and modify them to point to the coverage indexed version of the test file. -->
      		<copy todir="${testhtmdir}">
      			<fileset dir="${basedir}">
      				<include name="**/*.test.htm" />
      			</fileset>
      		</copy>		
      		
      		<!-- copy core resources to testhtmdir so we can load them with same paths as when executing test htm files directly -->
      		<copy todir="${testhtmdir}">
      			<fileset dir="${jsinstrumenteddir}">
      				<include name="**/*.js" />
      				<exclude name="jscoverage.js"/>
      			</fileset>
      		</copy>				
      		<copy todir="${testhtmdir}">
      			<fileset dir="${basedir}">
      				<include name="test-support.js" />
      				<include name="run-qunit.js" />
      				<include name="qunit.css" />
      				<include name="qunit.js" />
      			</fileset>
      		</copy>				
      	</target>
      	
      	<target name="jstest">
            <!--Run all tests w/phantom, fail if tests fail. Execute all files w/extension .test.htm. -->
            <apply executable="${basedir}/${phantom.filename}" failonerror="true" dir="${testhtmdir}" relative="false">
               <arg value="run-qunit.js"/>
      		 <srcfile/>
      		 <arg value="${basedir}"/>
               <fileset dir="${testhtmdir}">
                  <include name="**/*.test.htm" />
               </fileset>
            </apply>			
      	</target>
      	
      	<target name="main" depends="clean, prep, jstest">
      	</target>
      </project>
      
  5. Modify our test-support.js to look for jscoverage data and output a rough count of lines hit, missed, and irrelevant (non-executable). Also expose a function a caller outside of page context can use to access coverage information. The new version should look like this:
    • //create a scope so we don't pollute global
      (function() {  
         var testName;
         
         //arg: { name }
      	QUnit.testStart = function(t) {
      	    testName = t.name;
      	};
      	
      	//arg: { name, failed, passed, total }
      	QUnit.testDone = function(t) {
      	    console.log('Test "' + t.name + '" completed: ' + (0 === t.failed ? 'pass' : 'FAIL'))
      	};
      	
      	//{ result, actual, expected, message }
      	QUnit.log = function(t) {
      	    if (!t.result) {
      	        console.log('Test "' + testName + '" assertion failed. Expected <' + t.expected + '> Actual <' + t.actual + '>' + (t.message ? ': \'' + t.message + '\'' : ''));
      	    }
      	};
      	
      	//we want this at global scope so outside callers can find it. In a more realistic implementation we
      	//should probably put it in a namespace.
      	window.getCoverageByLine = function() {
      		var key = null;
              var lines = null;
              //look for code coverage data    
              if (typeof _$jscoverage === 'object') {
      			for (key in _$jscoverage) {}
      			lines = _$jscoverage[key];
              } 
      
      		if (!lines) {
                 console.log('code coverage data is NOT available');
              } 
              		
              return { 'key': key, 'lines': lines };
         };
      
         QUnit.done = function(t) {
              var cvgInfo = getCoverageByLine();
              if (!!cvgInfo.lines) {
                  var testableLines = 0;
                  var testedLines = 0;
      			var untestableLines = 0;
                  for (lineIdx in cvgInfo.lines) {
      				var cvg = cvgInfo.lines[lineIdx];
      				if (typeof cvg === 'number') {
      					testableLines += 1;
      					if (cvg > 0) {
      						testedLines += 1;
      					}					
      				} else {
      					untestableLines += 1;
      				}
                  }     
                  var coverage = '' + Math.floor(100 * testedLines / testableLines) + '%';
                  
      			var result = document.getElementById('qunit-testresult');
      			if (result != null) {
      				result.innerHTML = result.innerHTML + ' ' + coverage + ' test coverage of ' + cvgInfo.key;
      			} else {
      				console.log('can\'t find test-result element to update');
      			}			
              }
         };  	
      }());
      
  6. Finally, modify run-qunit.js to load the original js file and produce a colorized version based on the coverage data we get by running the test against the version of the js file indexed for coverage. The new version should look like this:
    • /**
       * Wait until the test condition is true or a timeout occurs. Useful for waiting
       * on a server response or for a ui change (fadeIn, etc.) to occur.
       *
       * @param testFx javascript condition that evaluates to a boolean,
       * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or
       * as a callback function.
       * @param onReady what to do when testFx condition is fulfilled,
       * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or
       * as a callback function.
       * @param timeOutMillis the max amount of time to wait. If not specified, 3 sec is used.
       */
      function waitFor(testFx, onReady, timeOutMillis) {
          var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3001, //< Default Max Timout is 3s
              start = new Date().getTime(),
              condition = false,
              interval = setInterval(function() {
                  if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) {
                      // If not time-out yet and condition not yet fulfilled
                      condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()); //< defensive code
                  } else {
                      if(!condition) {
                          // If condition still not fulfilled (timeout but condition is 'false')
                          console.log("'waitFor()' timeout");
                          phantom.exit(1);
                      } else {
                          // Condition fulfilled (timeout and/or condition is 'true')
                          console.log("'waitFor()' finished in " + (new Date().getTime() - start) + "ms.");
                          typeof(onReady) === "string" ? eval(onReady) : onReady(); //< Do what it's supposed to do once the condition is fulfilled
                          clearInterval(interval); //< Stop this interval
                      }
                  }
              }, 100); //< repeat check every 250ms
      };
      
      
      if (phantom.args.length === 0 || phantom.args.length > 3) {
          console.log('Usage: run-qunit.js URL basedir');
          phantom.exit(1);
      }
      
      var fs = require('fs');
      var page = require('webpage').create();
      
      // Route "console.log()" calls from within the Page context to the main Phantom context (i.e. current "this")
      page.onConsoleMessage = function(msg) {
          console.log(msg);
      };
      
      var openPath = phantom.args[0].replace(/^.*(\\|\/)/, '');
      var basedir = phantom.args[1];
      var coverageBase = fs.read(basedir + fs.separator + 'coverageBase.htm');
      
      page.open(openPath, function(status){
          if (status !== "success") {
              console.log("Unable to access network");
              phantom.exit(1);
          } else {
              waitFor(function(){
                  return page.evaluate(function(){
                      var el = document.getElementById('qunit-testresult');
                      if (el && el.innerText.match('completed')) {
                          return true;
                      }
                      return false;
                  });
              }, function(){
      			//BEGIN MODIFIED: output colorized code coverage
      			//reach into page context and pull out coverage info. stringify to pass context boundaries.
      			var coverageInfo = JSON.parse(page.evaluate(function() { return JSON.stringify(getCoverageByLine()); }));
      			var lineCoverage = coverageInfo.lines;
      			var originalFile = basedir + fs.separator + coverageInfo.key;
      			var fileLines = readFileLines(originalFile);
      			
                  var colorized = '';
                  
      			console.log('lines=' + JSON.stringify(lineCoverage));
                  for (var idx=0; idx < lineCoverage.length; idx++) { 
                      //+1: coverage lines count from 1.
                      var cvg = lineCoverage[idx + 1];
                      var hitmiss = '';
                      if (typeof cvg === 'number') {
                          hitmiss = ' ' + (cvg>0 ? 'hit' : 'miss');
                      } else {
                          hitmiss = ' ' + 'undef';
                      }
                      var htmlLine = fileLines[idx].replace('<', '&lt;').replace('>', '&gt;');
                      colorized += '<div class="code' + hitmiss + '">' + htmlLine + '</div>\n';
                  };        
                  colorized = coverageBase.replace('COLORIZED_LINE_HTML', colorized);
                  
                  var coverageOutputFile = phantom.args[0].replace('.test.htm', '.coverage.htm');
                  fs.write(coverageOutputFile, colorized, 'w');
                  
                  console.log('Coverage for ' + coverageInfo.key + ' in ' + coverageOutputFile);			
      			//END MODIFIED
      		
                  var failedNum = page.evaluate(function(){
                      var el = document.getElementById('qunit-testresult');
                      console.log(el.innerText);
                      try {
                          return el.getElementsByClassName('failed')[0].innerHTML;
                      } catch (e) { }
                      return 10000;
                  });
                  phantom.exit((parseInt(failedNum, 10) > 0) ? 1 : 0);
              });
          }
      });
      
      //MODIFIED: add new fn
      function readFileLines(filename) {
          var stream = fs.open(filename, 'r');
          var lines = [];
          var line;
          while (!stream.atEnd()) {
              lines.push(stream.readLine());
          }
          stream.close();
          
          return lines;
      }  
      
      
  7. Run 'ant'; you should see output similar to:

  8. Open \jsunit\target\testhtm\testme.test.htm in a browser; you should see something similar to this (note coverage % appears):

  9. Open \jsunit\target\testhtm\testme.coverage.htm in a browser; you should see something similar to this (red for untested, green for tested, light green for non-executable lines):

So where does that leave us?
We have clearly displayed we can accomplish some important things:


  • Write unit tests for Javascript
  • Run unit tests for Javascript in a command line build
  • Index Javascript files for code coverage
  • Output coverage percentage to the test runner (QUnit html file)
  • Render a colorized version of the Javascript under test clearly indicating which lines are/aren't being tested
I think this is awesome! Bear in mind in a real version we would of course make numerous refinements to this rather basic implementation; what we have is a proof of concept not by any stretch of the imagination an implementation ready for a team to consume.

87 comments:

Jackie Bolinsky said...

Hi there, I’m not a CI user, but I thought I’d sign up and tell you that the problem is actually with Doctrine cli not correctly passing the configuration to the generatemodelsdb task.
JavaScript Countdown Timer

Anonymous said...

have you ever tried writing the add function like this:

function add() {
var sum = 0;
for(var i in arguments){
sum += arguments[i];
}
return sum;
}

The test fails when I run it from the CL. In the browser the test is OK.

Eric said...

Check this out, it adds sweet colors to the pass/fail output. Totally ganked it from https://gist.github.com/1588423, but it's still nice.

/*global QUnit */

//create a scope so we don't pollute global
(function(QUnit) {
var testName
, displayPass = '\033[1;92mPass\033[0m'
, displayFail = '\033[1;31mFail\033[0m';

//arg: { name }
QUnit.testStart = function(t) {
testName = t.name;
};

//arg: { name, failed, passed, total }
QUnit.testDone = function(t) {
console.log((0 === t.failed ? displayPass : displayFail) + ': ' + t.name);
};

//{ result, actual, expected, message }
QUnit.log = function(t) {
if (!t.result) {
console.log(' ' + testName + ' assertion failed. Expected <' + t.expected +
'> Actual <' + t.actual + '>' + (t.message ? ': \'' + t.message + '\'' : ''));
}
};
}(QUnit));

Eric said...

Actually, this too. You want it to handle .equal and .ok both, I think:

/*global QUnit */

//create a scope so we don't pollute global
(function(QUnit) {
var testName
, displayPass = '\033[1;92mPass\033[0m'
, displayFail = '\033[1;31mFail\033[0m';

//arg: { name }
QUnit.testStart = function(t) {
testName = t.name;
};

//arg: { name, failed, passed, total }
QUnit.testDone = function(t) {
console.log((0 === t.failed ? displayPass : displayFail) + ': ' + t.name);
};

//{ result, actual, expected, message }
QUnit.log = function(t) {
if (!t.result) {
if (t.expected) {//Only print this if it's there (works for 'equal' assertions)
console.log (' ' + 'Expected "' + t.expected + '" Actual "' + t.actual + '"');
}
console.log( ' ' + testName + ' assertion failed: ' + (t.message ? t.message : ''));
}
};
}(QUnit));

http://www.simpleascouldbe.com

Eric said...

Great instructions, thank you.

I ended up getting this to work in a way that is fairly elegant for our multi-platform maven system. I followed your instructions, with these enhancements to promote automation:

* I put installs of phantom-win and phantom-osx in the source tree so that run scripts could always depend on their location.
* I used maven-exec-plugin to run the .bat or .sh file, depending on the environment (developer needs to manually set the path to the batch file in ~/.m2/settings.xml, but that's a one time thing). This is attached to the integration-test phase.

This makes it so that after SCM update, the developer only needs to change the path to the executable in order for this to be automated.

http://www.simpleascouldbe.com

Anonymous said...

Great Blog Easy to understand lot of Things.
Really useful for Web Developers. Wordpress development company in chennai

ecommerce website development services

Best web design company in chennai

ecommerce website design services

Zinavo-Web Design | Web Development | SEO | Mobile Apps | ERP/CRM said...

Nice post. Thanks for sharing information about your services. This is really useful. Web Design & Development Company in Bangalore | Website Design Company in Bangalore | Web Designing Company in Bangalore | Web Design Companies in Bangalore

Samuel Alex said...

Thanks so much for sharing this awesome info! I am looking forward to see more posts by you! Check out our websites,


Antivirus || Clean my PC || Antivirus Software || Antivirus Cleaner || Internet Security || Antivirus Protection || Internet Security Software || Antivirus Download || Security Antivirus || Antivirus and Security Software || Install Antivirus || Antivirus & Security Software || Mobile Security and Antivirus ||

Brandstory said...

Nice post Really useful information  Web Design Company in Bangalore, Web Development Company in Bangalore, Web Design Services in Bangalore, Web Design Company in Bangalore, Best Website Design Companies in Bangalore, Best Website Development Company in Bangalore, Website Design Services in Bangalore, Best Website Design Company in Bangalore, SEO Company in Bangalore, SEO Agency in Bangalore, SEO Services in Bangalore, Best SEO Companies in Bangalore, SEO Services Company in Bangalore

Anonymous said...

Nice Blog!! Thanks For Sharing Wonderful Post.
Digital Marketing | Outsource SEO Services | Digital Marketing Services | Digital Marketing in India | Facebook Ads | Lead Generation Services

Rahul said...

Nice Article..Thanks for the information..found ir really interesting

Python Training in Chennai
Python Training Institute in Chennai
Python Training in Chennai Anna Nagar
Python Training in Chennai OMR
UiPath Training in Chennai
Informatica Training in Chennai
Informatica Training Center Chennai
Informatica Training Center in Chennai

Desinelabs said...

Thanks for the information...

seo services bangalore

Desinelabs said...

Thanks for sharing your info. I really appreciate your efforts and I will be waiting for your further write ups thanks once again...
Digital Marketing Company in Bangalore
Digital Marketing Agencies in Bangalore

Anonymous said...
This comment has been removed by the author.
YorviTech said...

Learning basics being a Developer is very necessary. Thanks for sharing this article, share more in future.

Anurag Srivastava said...

Hey, Your post is very informative and helpful for us...Postal Recruitment 2020 is a leading job website for all Government job recruitment notification. This page is an exclusive page for the Latest recruitment notification from India Post India Post 2020...

My Digital Leader said...

Good one. Thanks for sharing
Digital Marketing company in Bangalore l Web Design Company in Bangalore l Web Development Company in Bangalore l Advertising Agencies in Bangalore l SEO Company in Bangalore l SEO Company in Bangalore l Digital Marketing Companies in Bangalore l Ecommerce Website Development in Bangalore

Techiesys said...

Good content seo company in Bangalore

Techiesys said...

Amazing information shared.

SEO agency in Bangalore

Pooja said...

To Know FreeJobAlert and to know more about upcoming jobs .

Quikks Digital Solutions said...

Good Technical Blog.Are you looking to revamp or optimize your blog?
We are a Seo Firm Bangalore.Our Senior SEO Consultant have a 10+ years experience, offer services like
SEO,PPC,SMM,online reputation management & web development both to small and medium companies.As a running offer,we are providing free performance audit for our customers

swati singh said...

Thank you for sharing your information i like that your content and your writing way. keep sharing your content and also write a blog about the invest in startups. Keep posting!

Brandstory said...

Thanks for your post! Really interesting blogs. Here is the some more interesting and most related links.

Best digital marketing company in Dubai, United Arab Emirates. Brandstory is one of the top and best digital marketing companies in Dubai UAE. As a leading digital marketing agency in Dubai, We offer search engine optimization services, online marketing services, UI UX design services, search engine marketing services, email marketing services, Google / Facebook / Bing pay per click services, Internet marketing services, website design services and website development services, social media marketing services. Hire ROI based digital marketing services company in dubai to get digital leads for your business.

Digital marketing company in Dubai | Digital Marketing Agency in Dubai | SEO Company in Dubai | SEO Agency in Dubai | Best Digital Marketing Companies in Dubai | Top Digital Marketing Agencies in Dubai | Best SEO Companies in Dubai | SEO Agencies in Dubai | Online Marketing Company in Dubai | SEO Services Company in Dubai | PPC Company in Dubai | PPC Agency in Dubai | PPC Services in Dubai | Social Media Marketing Company in Dubai | Social Media Marketing Services in Dubai | Social Media Marketing Agencies in Dubai | Web Design Company in Dubai | Website Designers in Dubai | Website Development Services Company in Dubai | Web Design Companies in Dubai

హాట్‌గర్ల్స్ said...

It's Really A Great Post. Looking For Some More Stuff.

Digital marketing agency in hyderabad | Digital marketing

companies in hyderabad

హాట్‌గర్ల్స్ said...

he post was really very good.Thanks for sharing.

SEO company in bangalore | SEO services in

bangalore

Unknown said...

SIAUAE is one of the top audit firms in UAE with highly experienced professionals. As a leading vat consultancy services in uae, we achieved the label of best VAT consultant in UAE.

Looking for the top accounting firms in UAE? SIAUAE is the best accounting companies in uae , we offer comprehensive professional services in UAE.

SIAUAE management consultancy is a professional Management Consultancy UAE , Helping clients achieve their business goals. For more details call us now

Unknown said...

Today the companies are continually being challenged to reduce the expenses, most of working expenditures and other requirements. The main costs include the salary of employees like HR professional, accountant, auditors and administration. These problem can be overcome by best expert business outsourcing company. They provide quality and valuable services at low cost for the business enterprises. In UAE most of accounts payable outsourcing companies offers cost effective accounting services that will help the companies effectively in the current situation.
Audit firms in UAE

Unknown said...

The intensity of smells can't be exaggerated. Smell has such a solid association with one's passionate express that it revives the recollections that went with our experience, in a few occasions, returning over numerous years. A deliberately built fragrance can lift one's state of mind, and places one out of a positive and innovative outlook. Our fragrance arrangements give you a chance to make the ideal experience for your customers and associates – one in which they remain drew in and associated.
Aroma diffuser

Unknown said...

Good Blog, thanks for sharing
everybody want digital marketing company Kerala to have their organization on the highest point of the pursuit list and engage your business. We Provide Best Digital Services, Efficiently total start to finish center abilities without viable thoughts. Powerfully cultivate strategic arrangements without empowered worth. Interfacing your business to the clients who are keen on your administration, and make a chain of best digital marketing in Kochi.
Digital marketing company Kerala

Unknown said...

Good Blog, thanks for sharing
the intensity of smells can't be exaggerated. Smell has such a solid association with one's passionate express that it revives the recollections that went with our experience, in a few occasions, returning over numerous years. A deliberately built fragrance can lift one's state of mind, and places one out of a positive and innovative outlook. Our fragrance arrangements give you a chance to make the ideal experience for your customers and associates – one in which they remain drew in and associated.
Aroma diffuser

Online Training Courses said...

If you want to survive in the technology world, you have to learn python for data science. In order to gain knowledge, it is recommended to attend some training courses.

Online Training Courses said...

Dot net Information:

https://dotnetcourseonline.wordpress.com/
https://dotnetonlinetrainingcourse.blogspot.com/
https://sites.google.com/view/microsoft-certification-course/
https://netdevelopercourse.tumblr.com/
https://learndotnet.wixsite.com/learndotnet/

Online Training Courses said...

Data Science Online Training

https://datasciencecourse229747904.wordpress.com/
https://datasciencecourseinusa.blogspot.com/
https://sites.google.com/view/free-data-science-courses/
https://onlinetrainingusa.wixsite.com/datasciencecourses
https://bestdatasciencecourses.tumblr.com/

kseo.agency said...

ISO Certification in Delhi – Genveritas a global ISO Certification Consulting firm represents considerable authority in tweaked and result-situated answers for assisting organizations to actualize change and improve business execution.

Neuro Doctors are a cohesive group of Top Neurosurgeon in Bangalore Neurologists, Intervention Neuroradiologist, pain management specialists who work together to provide comprehensive neurosciences care to our patients.

KEEN SEO Agency – Best Web Design Company in Bangalore . We provide full-service Web Design & Development Solutions that also includes specialized SEO services for Small Businesses. We offer Strategist Local SEO, Ecommerce SEO, website auditing, Paid Search (PPC) strategies including Google Ads, Facebook & Linked In Ads for Small Business (B2B & B2C).

John said...

Great article shared.Web Development company Kochi

Anonymous said...
This comment has been removed by the author.
DigiRoads said...


HI guys,
This is a very good post, and I like it very much. For us, it's insightful and helpful. For a long time, I've been looking for this and I'm just really pleased to say that I'm working with this stuff as well. Thanks for sharing this with us.
Digital Marketing Company in Jaipur
Digital Marketing Company In Bangalore
seo company In Delhi

Best Agriculture Financial Company in India said...

great blog. Really love your content do check Agricultural Investment Company

Best Agriculture Financial Company in India

Aadi Web Solutions said...


Aadi Web Solutions Is a well-established website designing company in Faridabad with 8 years of solid experience.website designing. Every Web design & development needs a different approach to build that’s why we have experts of every kind. We are flexible, versatile, and the pro at it. Our experts are working on CMS that is multi-functional, scalable, and dynamic in web development by utilizing the high end technologies like ASP.Net/PHP. We develop websites for small and Enterprise level business. We are best Web Designing Company In Faridabad If you want to propel your business forward.

mani said...

I really like reading through a post that can make people think. Also, many thanks for permitting me to comment!

Best SEO Company in Bangalore|
SEO Services Company In Bangalore|
SEO Services In Bangalore|
Mobile App Development Companies In Bangalore|
Best Mobile App Development Companies In Bangalore|

Abishek said...

Nice post it is really an interesting article.We are also providing the web design services in mumbai. We are the leading
website development company in mumbai
web design company in mumbai

Gorish dua said...

Thanks for sharing.
We at Antino Labs believe in redefining and refining our model to suit the industry's requirements. Antino Labs' several years of experience in the market has let us register our global presence. Antino Labs' has the vision to become the world's most trusted partner for digital transformation and we aim to become a brand that defines innovation and the latest technology. We offer clients a one-stop solution for all their interests regarding IT consulting services in Gurgaon and UI/UX design services in Gurgaon.

ISO 9001 Certification in South Africa said...

hello very nice blog. it is really an interesting article we are providing some backlinks. I hope these content useful for you.
backlinks
backlinks
backlinks
backlinks
backlinks
backlinks
backlinks
backlinks

ISO Implementation in Saudi Arabia said...

Hi this do follow backlink this has a amazing content, thanks for sharing
Do Backlinks
Do Backlinks
Do Backlinks

ISO Consultant in Singapore said...

Hi, thanks for sharing this amazing content.
Backlinks
Backlinks
Backlinks
Backlinks

ISO Consultant in Singapore said...

Great article with fantastic notion I admire your submit thankyou so lots and let preserve on sharing your stuff
Backlinks
Backlinks
Backlinks
Backlinks

ISO 9001 Certification in South Africa said...

hello every one nice blog. it is really an interesting article here providing some backlinks.
backlinks
backlinks
backlinks
backlinks
backlinks
backlinks

ISO Consultant in Singapore said...

Hello everyone this information is very useful for us. We are Hoping that you will continue posting such an useful backlinks having valid information
Backlinks
Backlinks

ISO Consultant in Singapore said...

Hello everyone These are the top Digital Marketing organizing organizations in Dubai business viability with effective development and arrangement.
Backlinks
Backlinks
Backlinks
Backlinks

ISO 9001 Certification in South Africa said...

hello every one nice blog. it is really an interesting article We are also providing some backlinks.
backlinks
backlinks
backlinks
backlinks
backlinks
backlinks

ISO Consultant in Singapore said...

Hi everyone Thanks for your post! Really fascinating blogs. Here is the some greater fascinating and most associated links.
Backlinks
Backlinks

Anonymous said...

very informative article about "integrating javascripts" found it very useful...

B2B Appointment Setting Services

Architectural CAD Drafting said...


HTML pages are usually integrated with JavaScript functions. ... JavaScript is the main scripting language used to make web pages dynamic, very informative article about "javascript"

Architectural CAD services

ISO Consultant in Singapore said...

Hello, your blog is greater informational for me thanks for sharing such kind of content material really. It is a very beneficial and information helpful content material like us.
Backlinks
Backlinks

ISO 9001 Certification in South Africa said...

Very Nice post it is really an interesting article. this informative backlink
backlinks
backlinks
backlinks
backlinks
backlinks
backlinks

Anonymous said...

thanks for sharing very informative post about "integrating javascript tests"

Outsourced Lead Generation Services

Architectural CAD Drafting said...

must appreciate very informative post about "integrating java scripts"

BIM service providers

ISO 9001 Certification in South Africa said...

Hello Everyone! Great article! This is very necessary archives for us. I like all content material fabric and information. I have observe it. You recognize more about this please go to again
backlinks
backlinks
backlinks
backlinks
backlinks

Cre8tivebot said...

Good Job! You have Shared your well knowledge... I will refer the people to the best IT Solutions providers click the below link:

IT Company
seo packages
ppc company australia
content marketing agency dubai
social media marketing company
web development australia
app development melbourne

cityweb said...

Great blog.Thanks for sharing such a useful information
best seo company in bangalore

best seo services company in bangalore

Unknown said...
This comment has been removed by the author.
Unknown said...

Certified Digital Marketing course in Panchkula

Unknown said...

Digital Marketing Training Institute in Panchkula

Mona Mishra said...

Nice blog on JAVA.
Ecommerce development Company

Mona Mishra said...

nice post on java script .
We are helping eCommerce businesses by increasing their customer reach and growing revenue through the optimal combination tools, and user-centric solutions. Being India’s leading eCommerce software development company, we help B2B & B2C clients drive their customers’ satisfaction, expand their right potential audiences and boost sales upto 100%.
Ecommerce development Company

periyannan said...

Great explanation.. Thanks
Internship providing companies in chennai | Where to do internship | internship opportunities in Chennai | internship offer letter | What internship should i do | How internship works | how many internships should i do ? | internship and inplant training difference | internship guidelines for students | why internship is necessary

Village Talkies said...

A great blog, it has a lot of useful information to me
Village Talkies a top-quality professional corporate video production company in Bangalore and also best explainer video company in Bangalore & animation video makers in Bangalore, Chennai, India & Maryland, Baltimore, USA provides Corporate & Brand films, Promotional, Marketing videos & Training videos, Product demo videos, Employee videos, Product video explainers, eLearning videos, 2d Animation, 3d Animation, Motion Graphics, Whiteboard Explainer videos Client Testimonial Videos, Video Presentation and more for all start-ups, industries, and corporate companies. From scripting to corporate video production services, explainer & 3d, 2d animation video production , our solutions are customized to your budget, timeline, and to meet the company goals and objectives.
As a best video production company in Bangalore, we produce quality and creative videos to our clients.

Gratis School of Learning said...

Deal All, Are you looking IELTS exam preparation guidelines?? We provide Free IELTS Workshop in Panchkula.

Call today: +91 9887046666

diksha said...

rotavator price

rotavator price

Sarah Rob said...

Nice blog. Appreciate your thoughts. Ecommerce web development dubai

Meghana Sathish said...

Great blog, very informative blog on development tips. we are leading Graphic designing company in Bangalore.

Samuel Alex said...
This comment has been removed by the author.
Martin said...

We are a search engine optimization company in Dubai, UAE. SEO Services in Dubai We offer our best SEO services to businesses looking to rank higher on Google and get the attention they deserve

BEdigitech said...

Thanks for sharing. Bedigitech presenting SEO Services in India. As a result, our clients have achieved good search engine rankings with high-quality traffic.

Foo Click said...

I learned JavaScript in past 5 years ago. I found it very professional language and you can become front-end expert by getting expertise in HTML, CSS & JavaScript. Best Ecommerce Products

QuixkBooks Expert said...

Thank you for sharing the wonderful guide. you may read my new vlog-
QuickBooks Error 181016 and 181021 and QuickBooks Black And White Screen

shrishtyunikart said...

Thank you for sharing the wonderful guide
List of Top Engineering Colleges in Ahmednagar

List of Top Engineering Colleges in Aurangabad

List of Top Engineering Colleges in Mumbai

List of Top Engineering Colleges in Nagpur

List of Top Engineering Colleges in Nashik

honolulu music lessons said...


I really like this blog. This is very helpful information, thanks for sharing Honolulu music lessons workshop provides guitar lessons as well as online guitar lessons for your convenience in Honolulu, Hawaii. Additionally, we offer lessons in piano, ukulele, drum & percussion, electric brass, and violin, as well as music theory, composition, and music therapy. Schedule your private guitar lessons today.

web design company said...

Nice blog. The process in which a web developer in Honolulu manages your website from start to finish, adding the latest content to keep your customers informed, and backing up your data. Hawaii webmaster services use SEO best practices to ensure that your site ranks highly. For more information, call 808-330-5506. Address:-350 Ward Avenue #262, Honolulu HI. 96814.

Super Glass Hawaii said...

When you get cracks or chips in your windshield repair in Honolulu , prevent them from becoming a bigger problem by visiting our service center in Honolulu, HI. SuperGlass Hawaii provide best Cracks & chips windshield repair in Honolulu. No matter what make or model car or truck you have, SuperGlass Hawaii can save you time and money on windshield repairs in Honolulu, HI, Call; 808–342–9000, Location: SuperGlass Windshield Repair of Hawai’i P.O. Box 8922 | Honolulu, HI 96830.

Finest Towing Oahu said...

Superb Blog. If you are looking for Junk Car Removal Hawaii Company? Finest Towing Oahu is here to provide you with the best towing service available. We provide 24 hour towing service for all types of vehicles and models. Our junk car removal air services also give you a fair price.

Finest Towing Maui said...

Thanks for sharing. Finest Towing Maui is a certified, professional operator’s team expert that provides heavy-duty towing and towing truck services in Maui car removal services as soon as possible, across the island of Maui. Please contact 808-744-1964.

cosmetify said...

Thanks for sharing. build your own brand with private label cosmetics manufacturers in india , skin care, hair care & personal care products manufacturer.

Tech_Engineer said...

This blog is useful as well as informative. Keep sharing such blogs I really like your posts.
Digital marketing company in Kharar

manveen digi world said...

"I am very happy to discover your post as it will become on top in my collection of favorite blogs to visit.
Manveen digi World."

BEdigitech said...

Thanks for Sharing. Unlock the full potential of your online presence with digital marketing services in Delhi. From strategic social media campaigns to search engine optimization and compelling content creation, Delhi's digital marketing experts help businesses thrive in the digital realm, attract targeted audiences, and achieve their marketing goals.

UPVC and Aluminium Windows said...

Araga Windows is a leading manufacturer of premium uPVC And Aluminium Windows Manufacturers in India ,

Post a Comment