Backbone.js + require.js is a common technology combination for early front-end SPA projects, providing the basic MVC framework and modularization capabilities respectively.

For such existing projects, which have been analyzed in previous articles, they often face problems such as unclear dependencies, confusing packaging, and lack of testing. When it is maintained and new requirements are developed, it is undoubtedly a feasible way to make gradual improvement in the TDD way, rather than demolish and start all over again, combining with its own characteristics.

This paper will try to use a reconstruction example to introduce a brick, explain how to apply the new JEST testing framework to it, and use ES6 class and other new means to upgrade backbone. View View components and improve the page structure, hoping to improve similar projects to play a role of opening ideas.

Concepts of testing and refactoring will not be introduced again. Please read the following articles first:

  • The whole – instance analysis refactor the code SanBanFu “https://mp.weixin.qq.com/s/dvuMmBnAMZz3ywdRXIMiyQ
  • “For components React for unit testing” https://mp.weixin.qq.com/s/oE944uljXsWbnJQPCqjYIA

Backbone.js/require.js technology stack review

The Require. Js modular

Js, the most common modular management tool in the days before Webpack.

It itself can provide AMD-spec JS modules, and provides the ability to load text templates through plug-ins.

In a real project, we use ES6 syntax and ESM module specification to write source files and translate them into UMD modules with Babel; Finally, it is packaged by the optimization tool R.js provided by require.js, and the module is loaded in the browser by require.js itself.

Of course, using ES6 syntax and Babel is not necessary, and AMD can also implement testing.

About the development of modular can refer to this article (https://mp.weixin.qq.com/s/WG_n9t4E4q0kBWczkSEdEA)

Backbone.js

Unlike Angular, which provides a complete solution, backbone. js provides a very basic and free MVC framework that not only organizes projects in a variety of ways, but also allows you to replace parts of them.

Its main functional modules include:

  • Events: Provides functions such as binding and triggering of a series of Events
  • Model: transform, verify, calculate derived values of data or state, provide access control, and also be responsible for remote synchronization of data, and have event trigger mechanism; Function is similar to MobX
  • Collection: a Collection of models
  • Router: Provides the front-end routing function of SPA and supports hashChange and pushState
  • Sync: Some methods for remote requests
  • View: A View component that can assemble template data, bind events, and so on

In our actual project, the View layer supports both backbone. View and earlier react@13, which demonstrates its flexibility.

Backbone projects can also ignore the react part of the article.

Upgrading the test framework

As with the examples in the previous article, Jest is used as the testing framework.

The original use case

There was actually some unit testing code in the early projects, mostly with Jasmine testing part of the Model/Collection. Since Jest has Jasmine2 built in, this part of the syntax is not a problem and can be migrated painlessly.

The main problems with the earlier tests were:

  • One is not integrated into the workflow, using a separate web page as a carrier, and over time this step will be forgotten, use cases will fail, and new team members will not notice the work
  • Second, unit testing of the Model/Collection was lax and relied on a PHP server environment that provided mock data
  • Third, because the view layer is not well componentized, there is a lack of testing of view components

Jest for Backbone practice

Jest is a relatively new testing framework with zero configuration by default, but it also provides flexible adaptation methods that can adapt to various projects, including backbone. js.

The @ captbaritone young brother provides a very good interpretation of the video (need science online https://www.youtube.com/watch?v=BwzjVNTxnUY&t=15s), And with the example code (https://github.com/captbaritone/tdd-jest-backbone).

Configure the necessary dependencies and mappings

//package.json "scripts": { "tdd": "cross-env NODE_ENV=test jest --watch", "test": "cross-env NODE_ENV=test jest", ... }, "devDependencies" : {" Babel - cli ":" ^ 6.0.0 ", "Babel - core" : "^ 6.26.0", "Babel - eslint" : "^ 6.1.2", "Babel - jest" : "^ 22.1.0", "Babel - preset - es2015" : "^ 6.24.1", "Babel - preset - react" : "^ 6.24.1", "Babel - preset - stage - 1" : "^ 6.24.1", "cross - env" : "^ 5.1.3", "enzyme" : "^ 3.3.0", "enzyme - adapter - react - 13" : "^ 1.0.3", "jest" : "^ 22.1.4", "jquery" : "^ 3.1.1 regenerator -", "runtime" : "^ 0.11.1", "sinon" : "^ 4.2.2", "grunt - run" : "^ 0.8.0",... },Copy the code
  1. Configure two NPM scripts for running tests live at development time and tests at build time
  2. In the target project, it was actually ES6 translation with Babel 5; But since the previous source code had been developed entirely in ES6 syntax (and some of the original AMD code had been automatically converted), we could have used the newer Babel 6 for testing

Added support for older react versions

//.babelrc

{
  "env": {
    "test": {
      "presets": [
	    "es2015"."stage-1"."react"]."plugins": []}}}Copy the code
//jest.config.js

moduleNameMapper: {
	"unmountMixin": "react-unmount-listener-mixin". },...Copy the code
  1. Enzyme – Adaptor – React -13 is used according to the target project
  2. Use cross-env to set the environment variable test to configure a.babelrc file for jest without affecting the production environment
  3. Map component names according to the original rules according to the specific situation in the project

Add unit tests to the build task

If only the tests are written, but exist separately and can only be executed with NPM test, then it is the same old mistake. Add it to your existing Grunt build workflow with the grunt-run plugin:

// Gruntfile.js

build: function() {
	grunt.task.run([
		'run:test'.'eslint'. ] ); }, run: {test: {
		cmd: /^win/.test(process.platform) ? 'npm.cmd' : 'npm',
		args: ['test']}},Copy the code

In subsequent build tasks, if any unit tests fail, the process will stop.

Test the model and collection

A model might look like this:

import Backbone from 'backbone';
	
const CardBinding = Backbone.Model.extend({
	urlRoot: _appFacade.ajaxPrefix + '/card/binding'.defaults: {
		identity: null.password: null
	},
	
	validate: function(attrs){
		if ( !attrs.identity ) {
			return CardBinding.ERR_NO_IDENTITY;
		}
		if(!/^\d{6}$/.test(attrs.password) ) {
			returnCardBinding.ERR_WRONG_PASSWORD; }}}); CardBinding.ERR_NO_IDENTITY ='err_no_identity';
CardBinding.ERR_WRONG_PASSWORD = 'err_wrong_password';
	
export default CardBinding;
Copy the code

Inject the global URL prefix in the test

You can see that the model relies on the attribute _appfacade.ajaxPrefix in several global variables

Start by writing a fake global object:

// __test__/fakeAppFacade.js

var facade = {
	ajaxPrefix: 'fakeAjax'. }; window._appFacade = facade; module.exports = facade;Copy the code

In the test suite, introduce this module before model:

// __test__/models/CardBinding.spec.js

import fakeAppFacade from '.. /fakeAppFacade';
import Model from "models/CardBinding";
Copy the code

Intercept asynchronous requests with sinon

If the address of the asynchronous request is fixed, the real request must be intercepted.

// backbone.js

// Set the default implementation of `Backbone.ajax` to proxy through to `$`.
// Override this if you'd like to use a different library.
Backbone.ajax = function() {
	return Backbone.$.ajax.apply(Backbone.$, arguments); }; .Copy the code

Backbone requests, including backbone.sync/model.fetch(), essentially call jQuery’s $. Ajax method (by default), which is the traditional XHR approach. Sinon is a good way to do this:

it('should fetch from server'.functionConst server = sinon.createFakeserver (); const server = sinon.createFakeserver (); server.respondImmediately =true;
	server.respondWith(
		"GET", 
		`${fakeAppFacade.ajaxPrefix}/card`,
		[
			200,
			{"Content-Type": "application/json"},
			JSON.stringify({
				errcode: 0,
				errmsg: 'ok',
				result: {
					"docTitle": "i am a member card"."card": {
						"id": 123}}})]); model = new Model(mockData); model.fetch(); expect(model.get('docTitle')).toEqual("i am a member card");
	expect(model.get('card')).not.toBeNull();
	expect(model.get('card').id).toEqual(123); Server.restore (); });Copy the code

Tests for validation operations

Calling the isValid() method of backbone. Model gets a Boolean value indicating whether the data isValid, and triggers the internal validate() method to update its validationError value. Using these features, we can do the following tests:

// Validate (attrs) {const re = new RegExp(attrs.field_username. Pattern);if ( !re.test(attrs.field_username.value) ) {
		returnattrs.field_username.invalid; }},...Copy the code
/ / in the spec

it('should validate username'.function(){
	let mock1 = {
		field_username: {
			pattern: '^[\u4E00-\u9FA5a-zA-Z0-9_-]{2,}$'.invalid: 'Please fill in your name correctly'
		},
		field_birth: {}}; model =new Model(mock1);

	model.set({
		'field_username': Object.assign(
			mock1.field_username, 
			{value: 'Yuchigong Hello123'})}); expect(model.isValid()).toBeTruthy();//trigger model.validate()
	expect(model.validationError).toBeFalsy();

	model.set({
		'field_username': Object.assign(
			mock1.field_username, 
			{value: 'his lieutenant'})}); expect(model.isValid()).toBeFalsy(); expect(model.validationError).toEqual('Please fill in your name correctly');

	model.set({
		'field_username': Object.assign(
			mock1.field_username, 
			{value: 'his lieutenant ~ 22'})}); expect(model.isValid()).toBeFalsy(); expect(model.validationError).toEqual('Please fill in your name correctly');
});
Copy the code

The tests for the Collection are nothing special compared to the Model, so I won’t go into details

The testable testable testable method from view

As mentioned in the opening paragraph, the previous obsolete test cases in the project were missing the view layer section.

On the one hand, this is due to the lack of testing consciousness at that time, and the more important reason is that the problem of componentization cannot be solved well.

To test a View, you need to break it down and refactor it into small, functional components that are easy to reuse.

Backbone.View’s ES6 class evolution

First of all, similar to the evolution of React. CreateClass to class extends Component, backbone. View can also turn around gracefully.

The traditional view is written like this:

const MyView = Backbone.View.extend({

	id: 'myView'.urlBase: _appFacade.ajaxPrefix + '/info'.events: {
		'click .submit': 'onSubmit'
	},

	render: function() {... },onSubmit: function () {... }});Copy the code

Using the ES6 class notation, it might be:

class MyView extends Backbone.View {
	get className() {
		return 'myComp';
	}
	get events() {
		return {
			"click .multi": "onShowShops"
		};
	}
	render() {
		const html = _.template(tmpl)(data);
		this.$el.html(html);
		return this;
	}
	onShowShops(e) {
		let cityId = e.currentTarget.id;
		if(cityId){ ... }}}Copy the code

Component extraction

Many pages of the target project do not reasonably encapsulate the sub-components, but only extract the HTML that needs to be reused into templates, “assemble” multiple sub-templates in this page, or reuse with other pages. This is partly due to the “overfreedom” of Backbone, which does not provide a good componentization solution in the official website or general practice at the time, and only uses the dependent underscore to implement _.template().

In fact, this is the same as the dilemma faced by the early wechat small programs: due to the lack of componentalization methods, modules can only be encapsulated at several levels of JS/WXML/WXSS. It wasn’t until late 2017 (version 1.6.3) that applets got their own component componentization scheme.

Another difficulty is that backbone. View’s constructor/initialize “constructor” does not accept custom props parameters.

The solution is to carry out a certain outer encapsulation:

// components/Menu.js

import {View} from 'backbone'; . const Menu =({data}) = >{
	class ViewClass extends View {
		get className() {
			return 'menu_component';
		}
		render() {
			const html = template(tmpl)(data);
			this.$el.html(html);
			return this; }}return ViewClass;
};
Copy the code

You can also “inherit” a View:

// components/MenuWithQRCode.js

import Menu from './Menu'; . const onQRCode =(e) = >{... };const MenuWithQRCode = ({data}) = >{
	const MenuView = Menu({data});
	class QRMenuView extends MenuView {
		get events() {
			return {
				"click #qrcode": onQRCode
			}
		}
	}
	return QRMenuView;
};
Copy the code

When used in the page, pass the parameter to obtain the real backbone. View component:

const Menu1View = MenuWithQRCode({
	data: {
		styleName: "menu1".list: tdata.menu1,
	}
});
Copy the code

Call its render() method manually and add it to the DOM of the page view:

this.$el.find('.menu1_wrapper').replaceWith(
	(new Menu1View).render().$el
);
Copy the code

In this way, backbone. View components are encapsulated and nested to a large extent.

Test backbone. View

Backbone.View = $el; backbone. View = $el; backbone. View = $el;

it("Should render empty when stores are not displayed.".function() {
	const ViewClass1 = CardShops({});
	const comp1 = (new ViewClass1).render();
	
	expect(comp1.$el.find('.single').length).toEqual(0);
	expect(comp1.$el.find('.multi').length).toEqual(0);
});
Copy the code

Tests for method calls

Sinon, of course:

it('Should respond correctly to event callbacks and load child templates'.functionConst server = sinon.createFakeserver (); const server = sinon.createFakeserver (); server.respondImmediately =true; // Return server.respondwith ("GET", 
		`${fakeAppFacade.ajaxPrefix}/privilege/222`,
		[
			200,
			{"Content-Type": "application/json"},
			JSON.stringify({
				errcode: 0,
				errmsg: 'ok', result: { ... }})]); const spy = sinon.spy(); const spy2 = sinon.spy(); const ViewClass1 = CardPrivileges({ data:{ title:"Promotional Activities for Merchants",
			list: [{
				"id": 111,
				"title": '20% discount for VIP members'."icon": 'assets_icon_card_vip'}, {"id": 222,
				"title": 'Open your card and get a tin of Coke. Open your card and get a tin of Coke.'."icon": 'assets_icon_card_priv1'}, {"id": 333,
				"title": '50 yuan voucher for every 200'."icon": 'assets_icon_card_priv2'."hasNew": true}] }, privOpenHandler: spy, detailLoadedHandler: spy2, responseHandler: (data,callback)=>callback(data) }); const comp = (new ViewClass1).render(); // Simulate the second click, expecting the fake data comp above the use case.$el.find('.privileges>li:nth-of-type(2)>a').click();

	expect(spy.callCount).toEqual(1);
	expect(spy2.callCount).toEqual(1);
	
	expect(comp.$el.find('.privileges>li:nth-of-type(2)').hasClass('opened')).toBeTruthy();
	expect(comp.$el.find('.privileges>li:nth-of-type(2) .cont_common').length).toEqual(1);
	expect(comp.$el.find('.cont_common li:nth-of-type(3)').html()).toEqual("Valid until September 20, 2014");

	server.restore();
});
Copy the code

Handle templates introduced with the Require.js text plug-in

Backbone.js + require. js in the test of a small problem is that the page or component is usually used in the text. Js component template, ES6 form:

import cardTmpl from 'text! templates/card.html';
Copy the code

Since the test environment does not have require.js or Webpack, we have to find a way to hijack it and inject the correct results into the corresponding test module;

To do this, use the jest.domock () method. The disadvantage of using this method is that you can’t use ES6’s import syntax.

// jest.config.js

moduleNameMapper: {
	"text! templates/(.*)": "templates/The $1". },...Copy the code
// __test__/TmplImporter.js

const fs = require('fs');
const path = require('path');

export default {
	import: tmplArrs=>tmplArrs.forEach(tmpl=>{
		jest.doMock(tmpl, ()=>{
			const filepath = path.resolve(__dirname, '.. /src/', tmpl);
			return fs.readFileSync(filepath, {encoding: 'utf8'}); }); })}Copy the code
// __test__/components/CardFace/index.spec.js

const tmplImporter = require('.. /.. /TmplImporter').default;
tmplImporter.import([
	'templates/card/card.html', // There can be more than one, if the test suite is used in the write]); Default const CardFace = require('components/CardFace/index').default;
Copy the code

conclusion

  • Jest’s flexible configuration capabilities enable it to be easily applied to TDD development and refactoring of various types of existing projects
  • Previous use cases from other test frameworks can be quickly migrated to JEST
  • Backbone.View After ES6 upgrade and encapsulation, the backbone. View component can improve the page cleanliness and is successfully used in unit tests
  • Sinon.createfakeserver () can be used to block asynchronous requests in backbone. Model
  • Templates originally introduced with the text.js component of require.js are also well supported with jest. DoMock ()
  • Adding a unit test task to an existing build workflow ensures that the relevant code lasts beyond


(end)

Long press the QR code or search fewelife to follow us