📜 ⬆️ ⬇️

TDD in javascript. Application development

Hello. This article focuses on Development Testing Methodology (TDD) using JavaScript.
I will write only briefly about the TDD methodology, more detailed information can be found from the link above.

Development through testing means that before writing code you need to:


Development through testing

So, closer to the topic.
')

Technical means


Library for testing : QUnit is an easy-to-use library with all the necessary functionality.

Object concept


Conceptually, we will divide all objects in the system into 3 categories:
  1. Basic . These are the classes that form the core of the system, are responsible for working with repositories (for example, GoogleGears, IndexedDb, etc.), interaction between modules, language packs, user settings, etc.
  2. Modules These are classes in which the main functionality of a module is implemented (for example, a module for displaying call history, a chat module, etc.)
  3. Auxiliary objects . Objects that have a small functionality and solve a specific small task (for example, an object showing a tooltip, a validator of fields, etc.)

Why divide? The type of object will depend on the completeness of the test coverage of its functionality. Basic objects are maximal, Modules are about 70%, Auxiliary objects are only for designing an object (more on this later) and checking the result.

Object naming


In this case, we use the simplest name:
All application objects will start with the word Application.
  1. Basic - Application_Core_ObjectName
  2. Modules - Application_Module_ModuleName
  3. Helper Objects - Application_Helper_ModuleName (if the object only works with a specific module) _ObjectName

There are many more interesting ways of naming, incl. and hiding names from the global scope, but this article is not about that.

Realization of objects


Because I like the principle “Simple is better than complicated” (c), then the implementation of objects will be as simple as possible and (which is very important!) compatible with the TDD methodology:

Base and Modules
function Application_Core_FuncName() {

this .publicMethod = function () {
}

this ._privateMethod = function () {
}

}


* This source code was highlighted with Source Code Highlighter .
function Application_Core_FuncName() {

this .publicMethod = function () {
}

this ._privateMethod = function () {
}

}


* This source code was highlighted with Source Code Highlighter .
function Application_Core_FuncName() {

this .publicMethod = function () {
}

this ._privateMethod = function () {
}

}


* This source code was highlighted with Source Code Highlighter .



Auxiliary objects
var Application_Helper_HelperName = {
init: function () {
},

publicMethod: function () {
},

_privateMethod: function () {
}
};


* This source code was highlighted with Source Code Highlighter .
var Application_Helper_HelperName = {
init: function () {
},

publicMethod: function () {
},

_privateMethod: function () {
}
};


* This source code was highlighted with Source Code Highlighter .
var Application_Helper_HelperName = {
init: function () {
},

publicMethod: function () {
},

_privateMethod: function () {
}
};


* This source code was highlighted with Source Code Highlighter .


Why exactly? Let's start with the problem statement: the application will be developed using the TDD methodology, i.e. It is necessary to implement access to private methods of classes for testing (there are several opinions about the need to test private methods. I am testing). According to generally accepted standards, a method in JavaScript that starts with an underscore is private (many IDEs do not show it in the hinta of available methods), but this method is actually available - the error will not be thrown when accessing, say, Application_Helper_HelperName._privateMethod() .

Development


Suppose we need to develop some kind of auxiliary module that should return the generated html with information about the user.

Let's start with the test:

module( "User Info" );

test( "main" , function () {
equals( typeof (Application_Helper_UserInfo), "object" , "Check object" );
})


* This source code was highlighted with Source Code Highlighter .
module( "User Info" );

test( "main" , function () {
equals( typeof (Application_Helper_UserInfo), "object" , "Check object" );
})


* This source code was highlighted with Source Code Highlighter .
module( "User Info" );

test( "main" , function () {
equals( typeof (Application_Helper_UserInfo), "object" , "Check object" );
})


* This source code was highlighted with Source Code Highlighter .


Run the test.

Obviously, the test fails, because there is no such object.
Create an object

var Application_Helper_UserInfo = {

};


* This source code was highlighted with Source Code Highlighter .
var Application_Helper_UserInfo = {

};


* This source code was highlighted with Source Code Highlighter .
var Application_Helper_UserInfo = {

};


* This source code was highlighted with Source Code Highlighter .


The test passed, proceed to the next test.
This object belongs to the type of auxiliary objects , i.e. tests will be written only to check the result and design.
What is design using TDD methodology? This is a description of class methods and their interaction using tests.

module( "User Info" );

test( "main" , function () {
equals( typeof (Application_Helper_UserInfo), "object" , "Check object" );

equals(Application_Helper_UserInfo.hasOwnProperty( "getHTML" ), true , "Check existing method getHTML" );

});


* This source code was highlighted with Source Code Highlighter .
module( "User Info" );

test( "main" , function () {
equals( typeof (Application_Helper_UserInfo), "object" , "Check object" );

equals(Application_Helper_UserInfo.hasOwnProperty( "getHTML" ), true , "Check existing method getHTML" );

});


* This source code was highlighted with Source Code Highlighter .
module( "User Info" );

test( "main" , function () {
equals( typeof (Application_Helper_UserInfo), "object" , "Check object" );

equals(Application_Helper_UserInfo.hasOwnProperty( "getHTML" ), true , "Check existing method getHTML" );

});


* This source code was highlighted with Source Code Highlighter .


And implementation:
var Application_Helper_UserInfo = {

getHTML: function () {
}

};


* This source code was highlighted with Source Code Highlighter .
var Application_Helper_UserInfo = {

getHTML: function () {
}

};


* This source code was highlighted with Source Code Highlighter .
var Application_Helper_UserInfo = {

getHTML: function () {
}

};


* This source code was highlighted with Source Code Highlighter .



Further documentation shows that user information consists of: photos, names and detailed information. Therefore, we design this way:

module( "User Info" );

test( "main" , function () {
equals( typeof (Application_Helper_UserInfo), "object" , "Check object" );

equals(Application_Helper_UserInfo.hasOwnProperty( "getHTML" ), true , "Check existing method getHTML" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getPhoto" ), true , "Check existing private method _getPhoto" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getUsername" ), true , "Check existing private method _getUsername" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getInfo" ), true , "Check existing method _getInfo" );

});


* This source code was highlighted with Source Code Highlighter .
module( "User Info" );

test( "main" , function () {
equals( typeof (Application_Helper_UserInfo), "object" , "Check object" );

equals(Application_Helper_UserInfo.hasOwnProperty( "getHTML" ), true , "Check existing method getHTML" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getPhoto" ), true , "Check existing private method _getPhoto" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getUsername" ), true , "Check existing private method _getUsername" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getInfo" ), true , "Check existing method _getInfo" );

});


* This source code was highlighted with Source Code Highlighter .
module( "User Info" );

test( "main" , function () {
equals( typeof (Application_Helper_UserInfo), "object" , "Check object" );

equals(Application_Helper_UserInfo.hasOwnProperty( "getHTML" ), true , "Check existing method getHTML" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getPhoto" ), true , "Check existing private method _getPhoto" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getUsername" ), true , "Check existing private method _getUsername" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getInfo" ), true , "Check existing method _getInfo" );

});


* This source code was highlighted with Source Code Highlighter .


We start - new tests have not passed.


We write implementation.

var Application_Helper_UserInfo = {

getHTML: function () {
},

_getPhoto: function () {
},

_getUsername: function () {
},

_getInfo: function () {
}
};


* This source code was highlighted with Source Code Highlighter .
var Application_Helper_UserInfo = {

getHTML: function () {
},

_getPhoto: function () {
},

_getUsername: function () {
},

_getInfo: function () {
}
};


* This source code was highlighted with Source Code Highlighter .
var Application_Helper_UserInfo = {

getHTML: function () {
},

_getPhoto: function () {
},

_getUsername: function () {
},

_getInfo: function () {
}
};


* This source code was highlighted with Source Code Highlighter .


Now you need to test the implementation of the methods. For testing, we will create a mock object (a stub for testing) and conduct testing in its context.

module( "User Info" );

test( "main" , function () {
equals( typeof (Application_Helper_UserInfo), "object" , "Check object" );

equals(Application_Helper_UserInfo.hasOwnProperty( "getHTML" ), true , "Check existing method getHTML" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getPhoto" ), true , "Check existing private method _getPhoto" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getUsername" ), true , "Check existing private method _getUsername" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getInfo" ), true , "Check existing method _getInfo" );

var mockUserInfo = {
username: 'Name' ,
photo: 'photo.png' ,
info: 'Information'
};

var photo = Application_Helper_UserInfo._getPhoto.call(mockUserInfo);

equals(photo, 'photo.png' , 'Checking photo' );

var username = Application_Helper_UserInfo._getUsername.call(mockUserInfo);

equals(username, 'Name' , 'Checking username' );

var info = Application_Helper_UserInfo._getInfo.call(mockUserInfo);

equals(info, 'Information' , 'Checking information' );
});


* This source code was highlighted with Source Code Highlighter .
module( "User Info" );

test( "main" , function () {
equals( typeof (Application_Helper_UserInfo), "object" , "Check object" );

equals(Application_Helper_UserInfo.hasOwnProperty( "getHTML" ), true , "Check existing method getHTML" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getPhoto" ), true , "Check existing private method _getPhoto" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getUsername" ), true , "Check existing private method _getUsername" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getInfo" ), true , "Check existing method _getInfo" );

var mockUserInfo = {
username: 'Name' ,
photo: 'photo.png' ,
info: 'Information'
};

var photo = Application_Helper_UserInfo._getPhoto.call(mockUserInfo);

equals(photo, 'photo.png' , 'Checking photo' );

var username = Application_Helper_UserInfo._getUsername.call(mockUserInfo);

equals(username, 'Name' , 'Checking username' );

var info = Application_Helper_UserInfo._getInfo.call(mockUserInfo);

equals(info, 'Information' , 'Checking information' );
});


* This source code was highlighted with Source Code Highlighter .
module( "User Info" );

test( "main" , function () {
equals( typeof (Application_Helper_UserInfo), "object" , "Check object" );

equals(Application_Helper_UserInfo.hasOwnProperty( "getHTML" ), true , "Check existing method getHTML" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getPhoto" ), true , "Check existing private method _getPhoto" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getUsername" ), true , "Check existing private method _getUsername" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getInfo" ), true , "Check existing method _getInfo" );

var mockUserInfo = {
username: 'Name' ,
photo: 'photo.png' ,
info: 'Information'
};

var photo = Application_Helper_UserInfo._getPhoto.call(mockUserInfo);

equals(photo, 'photo.png' , 'Checking photo' );

var username = Application_Helper_UserInfo._getUsername.call(mockUserInfo);

equals(username, 'Name' , 'Checking username' );

var info = Application_Helper_UserInfo._getInfo.call(mockUserInfo);

equals(info, 'Information' , 'Checking information' );
});


* This source code was highlighted with Source Code Highlighter .



Tests were not performed, you need to write an implementation that will allow tests to pass. The basic principle of TDD is to write only the necessary for the test to pass the code. Nothing extra.

var Application_Helper_UserInfo = {

getHTML: function () {
},

_getPhoto: function () {

return this .photo;
},

_getUsername: function () {

return this .username;
},

_getInfo: function () {

return this .info;
}
};


* This source code was highlighted with Source Code Highlighter .
var Application_Helper_UserInfo = {

getHTML: function () {
},

_getPhoto: function () {

return this .photo;
},

_getUsername: function () {

return this .username;
},

_getInfo: function () {

return this .info;
}
};


* This source code was highlighted with Source Code Highlighter .
var Application_Helper_UserInfo = {

getHTML: function () {
},

_getPhoto: function () {

return this .photo;
},

_getUsername: function () {

return this .username;
},

_getInfo: function () {

return this .info;
}
};


* This source code was highlighted with Source Code Highlighter .


Because Since an object belongs to the Auxiliary objects type, then more detailed tests on the functionality of private methods will not be written. It remains only to write a test that checks the result of the object, in this case the getHTML method.

module( "User Info" );

test( "main" , function () {
equals( typeof (Application_Helper_UserInfo), "object" , "Check object" );

equals(Application_Helper_UserInfo.hasOwnProperty( "getHTML" ), true , "Check existing method getHTML" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getPhoto" ), true , "Check existing private method _getPhoto" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getUsername" ), true , "Check existing private method _getUsername" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getInfo" ), true , "Check existing method _getInfo" );

var mockUserInfo = {
username: 'Name' ,
photo: 'photo.png' ,
info: 'Information'
};

var photo = Application_Helper_UserInfo._getPhoto.call(mockUserInfo);

equals(photo, 'photo.png' , 'Checking photo' );

var username = Application_Helper_UserInfo._getUsername.call(mockUserInfo);

equals(username, 'Name' , 'Checking username' );

var info = Application_Helper_UserInfo._getInfo.call(mockUserInfo);

equals(info, 'Information' , 'Checking information' );

var html = Application_Helper_UserInfo.getHTML.call(mockUserInfo);

if (html != undefined && html.indexOf( 'id="application_helper_userinfo"' ) != -1 && html.indexOf( 'Name' ) != -1 && html.indexOf( 'photo.png' ) != -1 && html.indexOf( 'Information' ) != -1) {

ok( true , "HTML ok" );

} else {
ok( false , "HTML does not pass" );
}

});


* This source code was highlighted with Source Code Highlighter .
module( "User Info" );

test( "main" , function () {
equals( typeof (Application_Helper_UserInfo), "object" , "Check object" );

equals(Application_Helper_UserInfo.hasOwnProperty( "getHTML" ), true , "Check existing method getHTML" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getPhoto" ), true , "Check existing private method _getPhoto" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getUsername" ), true , "Check existing private method _getUsername" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getInfo" ), true , "Check existing method _getInfo" );

var mockUserInfo = {
username: 'Name' ,
photo: 'photo.png' ,
info: 'Information'
};

var photo = Application_Helper_UserInfo._getPhoto.call(mockUserInfo);

equals(photo, 'photo.png' , 'Checking photo' );

var username = Application_Helper_UserInfo._getUsername.call(mockUserInfo);

equals(username, 'Name' , 'Checking username' );

var info = Application_Helper_UserInfo._getInfo.call(mockUserInfo);

equals(info, 'Information' , 'Checking information' );

var html = Application_Helper_UserInfo.getHTML.call(mockUserInfo);

if (html != undefined && html.indexOf( 'id="application_helper_userinfo"' ) != -1 && html.indexOf( 'Name' ) != -1 && html.indexOf( 'photo.png' ) != -1 && html.indexOf( 'Information' ) != -1) {

ok( true , "HTML ok" );

} else {
ok( false , "HTML does not pass" );
}

});


* This source code was highlighted with Source Code Highlighter .
module( "User Info" );

test( "main" , function () {
equals( typeof (Application_Helper_UserInfo), "object" , "Check object" );

equals(Application_Helper_UserInfo.hasOwnProperty( "getHTML" ), true , "Check existing method getHTML" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getPhoto" ), true , "Check existing private method _getPhoto" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getUsername" ), true , "Check existing private method _getUsername" );

equals(Application_Helper_UserInfo.hasOwnProperty( "_getInfo" ), true , "Check existing method _getInfo" );

var mockUserInfo = {
username: 'Name' ,
photo: 'photo.png' ,
info: 'Information'
};

var photo = Application_Helper_UserInfo._getPhoto.call(mockUserInfo);

equals(photo, 'photo.png' , 'Checking photo' );

var username = Application_Helper_UserInfo._getUsername.call(mockUserInfo);

equals(username, 'Name' , 'Checking username' );

var info = Application_Helper_UserInfo._getInfo.call(mockUserInfo);

equals(info, 'Information' , 'Checking information' );

var html = Application_Helper_UserInfo.getHTML.call(mockUserInfo);

if (html != undefined && html.indexOf( 'id="application_helper_userinfo"' ) != -1 && html.indexOf( 'Name' ) != -1 && html.indexOf( 'photo.png' ) != -1 && html.indexOf( 'Information' ) != -1) {

ok( true , "HTML ok" );

} else {
ok( false , "HTML does not pass" );
}

});


* This source code was highlighted with Source Code Highlighter .


That's all. More detailed checks for non-existent parameters, etc. will not be performed, since the object is auxiliary, for base classes or modules it would be necessary to write additional tests that check the status, repeated calls, work with the DOM-tree and so on.

Now you need to write the final implementation of the object, because the test that was written did not pass.

var Application_Helper_UserInfo = {

getHTML: function () {
var html = '<div id="application_helper_userinfo">' ;
html += '<div>' + Application_Helper_UserInfo._getPhoto.call( this ) + '</div>' ;
html += '<div>' + Application_Helper_UserInfo._getUsername.call( this ) + '</div>' ;
html += '<div>' + Application_Helper_UserInfo._getInfo.call( this ) + '</div>' ;
html += '</div>' ;
return html;
},

_getPhoto: function () {

return this .photo;
},

_getUsername: function () {

return this .username;
},

_getInfo: function () {

return this .info;
}
};


* This source code was highlighted with Source Code Highlighter .
var Application_Helper_UserInfo = {

getHTML: function () {
var html = '<div id="application_helper_userinfo">' ;
html += '<div>' + Application_Helper_UserInfo._getPhoto.call( this ) + '</div>' ;
html += '<div>' + Application_Helper_UserInfo._getUsername.call( this ) + '</div>' ;
html += '<div>' + Application_Helper_UserInfo._getInfo.call( this ) + '</div>' ;
html += '</div>' ;
return html;
},

_getPhoto: function () {

return this .photo;
},

_getUsername: function () {

return this .username;
},

_getInfo: function () {

return this .info;
}
};


* This source code was highlighted with Source Code Highlighter .
var Application_Helper_UserInfo = {

getHTML: function () {
var html = '<div id="application_helper_userinfo">' ;
html += '<div>' + Application_Helper_UserInfo._getPhoto.call( this ) + '</div>' ;
html += '<div>' + Application_Helper_UserInfo._getUsername.call( this ) + '</div>' ;
html += '<div>' + Application_Helper_UserInfo._getInfo.call( this ) + '</div>' ;
html += '</div>' ;
return html;
},

_getPhoto: function () {

return this .photo;
},

_getUsername: function () {

return this .username;
},

_getInfo: function () {

return this .info;
}
};


* This source code was highlighted with Source Code Highlighter .





Tests have passed, we can do the final refactoring: add additional checks, css for a more beautiful design and so on. But the most important thing is that now we are sure that nothing will break. Almost sure. Of course, TDD is not a cure for all diseases, but for an ever-evolving project, it is simply indispensable.

The use of TDD methodology increases the development time by approximately 40%, but reduces the time of bug fixing by several times. Supporting and developing the project becomes much easier, it becomes more stable and consistent, and QA can only be thoroughly tested the interface. Just today, testing of two modules of my project was carried out - one using TDD, the second - without. The result speaks in favor of TDD - in the first QA module, only minor graphics problems were found in IE9, but in the second, an unpleasant bug that would have been discovered 100% during development through testing.

Also the actual problem of the developers is the lack of preliminary design. If the task seems simple, then many are immediately involved in the implementation, considering the design a waste of time. Designing is great. Many situations where it is shameful to show your final code could be avoided with the help of preliminary design.

On this, perhaps, finish. The article was more than I expected, but only a small part of the TDD in JavaScript is covered.
If you are willing, I can continue with TDD and Ajax, testing complex modules, asynchronous state testing, automatically generated mock objects and frameworks for this.

Thanks for attention. Until new meetings.

Source: https://habr.com/ru/post/119465/


All Articles