Bad Practices For Testing

Bad Practices That Make Code Hard to Test

Tight Functions


function redirectTo(url) {
	if (url.charAt(0) === "#") {
		window.location.hash = url;
	} else if (url.charAt(0) === "/") {
		window.location.pathname = url;
	} else {
		window.location.href = url;
	}
}

Why it is bad

The function is tightly coupled with external services or logics so that it will be not easy to concentrate on core logic of its own for testing. Another example is context-aware input:

function(client){
	var res = yield.client.request();
	//complex handling with payload which you want to test
}

//instead of 

function(payload){
	//complex handling with payload which you want to test
}

The more native (simple) input and output you code has, the more testable it is.

How to fix it

Make function’s input and output more native and less context-aware.

function _getRedirectPart(url) {
	if (url.charAt(0) === "#") {
		return "hash";
	} else if (url.charAt(0) === "/") {
		return "pathname";
	} else {
		return "href";
	}
}

function redirectTo(url) {
	window.location[_getRedirectPart(url)] = url;
}
And now we can write a simple test suite for _getRedirectPart:

test("_getRedirectPart", function() {
	equal(_getRedirectPart("#foo"), "hash");
	equal(_getRedirectPart("/foo"), "pathname");
	equal(_getRedirectPart("http://foo.com"), "href");
});

Require or Initialize External Services Within Scope Under Tests

var Mongo = require("Mongo");
var Redis = require("Redis");

class DataStore{
	constructor(){
		this.mongo = new Mongo();
		this.redis = new Redis();
	}
	
	//logics
}

Why it is bad

The cost of set up and control external dependencies is large and not possible in many cases, which leads to inflexible design and difficulties in test cases.

describe("DataStore", function(){
	it("should do something correctly", function(){
		//we can not easily set up test unless we have real replicated infrastructure
		//step 1 start redis
		//step 2 start mongo
		var ds = new DataStore();
		var res = ds.doSomething();
		expect(res).to.be("expected output");
	});
});

How to fix it

Dependency Injection

//constructor injection
class DataStore{
	constructor(mongo, redis){
		this.mongo = mongo
		this.redis = redis
	}	
	//logics
}

//test
describe("DataStore", function(){
	it("should do something correctly", function(){
		//mocked implementation, so that the interaction can be fully controlled
		var mongo = new MockedMongo();
		var redis = new MockedRedis();
		//control input and output of external dependencies
		redis.onCall("foo").returns("bar");
		mongo.onCall("hello").returns("world");
		//inject mocked dependencies
		var ds = new DataStore(mongo, redis);		
		var res = ds.doSomething();
		expect(res).to.be("expected output");
	});
});

Violating the Law of Demeter

Law of Demeter

class AddressValidator(){
	constructor(remote_service_client){
		this.remote_service_client = remote_service_client;
	}
	
	validate(){
		var user = yield remote_service_client.getCurrentUser();
		var address = user.getAddress();
		//logics to validate address
	}
}

Why it is bad

Umlimited scope of dependencies usually introduces a lot unnecessary noise and difficulties to test, as tests need to rebuild the whole object graph the scope just for a small part of it. This kind of violation does not only happen in constructor, but also in function interfaces.

describe("AddressValidator", function(){
	it("should let good address pass", function(){
		var mock_client = //a bunch of code to mock the whole client
		instruct(mock_client).to.response(/* user with good address*/);
		var validator = new AddressValidator(mock_client);
		expect(validate(mock_client)).to.be(true);
	});
});

How to fix it

Require scope of input as small as possible.

class AddressValidator(){
	constructor(){}	
	validate(address){		
		//logics to validate address
	}
}

//test
describe("AddressValidator", function(){
	it("should let good address pass", function(){		
		var validator = new AddressValidator();
		expect(validate("GoodAddress1")).to.be(true);
		expect(validate("GoodAddress2")).to.be(true);
		expect(validate("BadAddress1")).to.be(false);
	});
});

Global Variables, Singletons

class Logger{
	constructor(){		
		this.log_store = FileManager.getStore();//global singleton			 
	}
	//do something with log_store
}

Why it is bad

  • Global virables and singletons are usually hard to be replacable, and lifecycle of global objects is not aligned with the object under tests.
describe("Logger", function(){
	it("should stream logs to a file", function(){		
		var logger = new Logger();
		//how can test cases take full control of FileManager in order to perform test ???
	});
});

How to fix it

Don’t use any global variables and singleton inside the scope you want to test. Use dependency injection.

//from
constructor(){		
	this.log_store = FileManager.getStore();			 
}
//to
constructor(log_store){		
	this.log_store = log_store;			 
}

//from
constructor(){}
function log_verbose(msg){
	if(NODE_ENV === "development"){
		console.log(msg)
	}else{
		//do nothing
	}
}

//to
constructor(shouldLogVerbose){ this.shouldLogVerbose = shouldLogVerbose} //the desicion making is moved to centralized external scope, just imagine what if the desicion is based on "NODE_ENV + NODE_OTHER_FLAG" instead of "NODE_ENV" solely?  
function log_verbose(msg){
	if(shouldLogVerbose){
		console.log(msg)
	}		
}

Messy Inherience

Inherience is usually for two purposes: code reuse and flow control. But in both case, it is hard for writing tests if no super careful handle is taken.

class CDDrive{
	eject(){};
	inject(){};	
	readOneDisk(){
		this.eject();
		//wait for disk 
		this.inject();							
		//do CD decoding
		return;
	}
} 

//subclassing is for resue part of eject() and inject()
class DVDDrive extends CDDrive{
	readOneDisk(){
		this.eject();
		//wait for disk 
		this.inject();							
		//do DVD decoding
		return;
	}
		
	eject(){
		super();
		blinkRed();
	}
	
	inject(){
		super();
		blinkGreen();
	};	
}
describe("DVDDrive", function(){
	it("should be able to read a disk", function(){		
		var drive = new DVDrive();
		//how to mock super.eject and super.inject ???				 
	});
});
//subclassing for flow control
class SocialAuthenticator(){
	constructor(encrypted_user_info){
		this.encrypted_user_info = encrypted_user_info;		
	}
	auth(){
		this.user_info = yield this.decrypt(this.encrypted_user_info);
		this.context = yield this.prepareContext(); //bad: sophisticated dependency for subclasses
		
		var token = yield this.realAuth();
		
		if(this.finished){
			yield this.update(user_info, token);
		} else {
			throw new Error("not finish");
		}		
		
		return yield this.redirectToLandingPage();		
	}			
}

class FacebookAuthenticator extends SocialAuthenticator(){
	constructor(config, encrypted_user_info){
		super(encrypted_user_info)
		this.client = new FacebookClient(config);
	}
	realAuth(){
		let info = this.user_info;
		let credential = context.getCredential();		
		var res = yield this.client.auth({credential: credential, info: info});
		if(res.err){
			return null;
		}else{
			return res.data;
		}
		this.finished = true; //bad: use shared variables to communicate		
	}	
}

//test
describe("FacebookAuthenticator", function(){
	it("should return updated info for a valid user", function(){		
		var validator = new FacebookAuthenticator("encrypted_user_info","CONFIG"); //prepare encrypted_user_info is unnecessary and noisy and very hard to mock 
		validator.auth();// wait how can I mock this.prepareContext and make it correct for my cases?		
		
		//and how can I verify the result? as the flow is going to this.redirectToLandingPage						 
	});
});

Why it is bad

  • Inherience blurs the relation and responsibilities of different class
  • Inherience usually has some shared state communication
  • Inherience usually introduces some noise to set up and get it through.

How to fix it

Extract Interfaces and Do Composition


//use self-contained class to wrap functionalities
class MechanicController{
	eject();
	inject();
}

class CDDrive{
	constructor(machanic_controller){
		this.machanic_controller = machanic_controller;
	}	
	readOneDisk(){
		this.machanic_controller.eject();
		//wait for disk 
		this.machanic_controller.inject();							
		//do CD decoding
		return;
	}
} 

class DVDDrive {
	constructor(machanic_controller, lightning_controller){
		this.machanic_controller = machanic_controller;
		this.lightning_controller = lightning_controller;
	}
	readOneDisk(){
		this.machanic_controller.eject();
		this.lightning_controller.blinkRed();
		//wait for disk 
		this.machanic_controller.inject();
		this.lightning_controller.blinkGreen();							
		//do DVD decoding
		return;
	}					
}

//test
describe("DVDDrive", function(){
	it("should be able to read a disk", function(){
		var mock_machanic_controller = {
			//mocking impl
		}
		
		var mock_lightning_controller = {
			//mocking impl
		}			
		var drive = new DVDrive(mock_machanic_controller, mock_lightning_controller);		
		var res = drive.readOneDisk();
		
		//assertion here	 
	});
});

class SocialAuthenticator(){
	constructor(encrypted_user_info, social_auth_client){
		this.encrypted_user_info = encrypted_user_info;
		this.social_auth_client = social_auth_client; //this is an interface for all implementation for different social network, which has one method: doAuth = (user, credential) => {isValid , user_token, err}		
	}
	auth(){
		this.user_info = yield this.decrypt(this.encrypted_user_info);
		this.context = yield this.prepareContext();
		
		let credential = context.getCredential();		
		var res = yield this.social_auth_client.doAuth({credential: credential, user: user});
		
		if(res.isValid){
			yield this.update(user_info, res.token);
		}else{
			throw new Error("not valid");
		}
		
		return yield this.redirectToLandingPage();				
	}			
}

class FacebookAuthenticator extends SocialAuthenticator(){
	constructor(config){		
		this.client = new FacebookClient(config);
	}
	
	doAuth(credential, user){					
		var res = yield this.client.auth({api_key:credential.api_key, info: user});
		if(res.err){
			return {isValid:false, err: res.err};
		}else{
			return {isValid:false, token: res.data.token};
		}				
	}	
}

//test
describe("FacebookAuthenticator", function(){
	it("should return updated info for a valid user", function(){		
		var validator = new FacebookAuthenticator("CONFIG"); 
		
		//now you can focus on your Facebook impl without any noisy and extra set up
		
		expect(yield validator.doAuth({credential:"Bad credential"}, user:{id:"", token:""}).to.has("err").euqals("Invalid credential");
		
		expect(yield validator.doAuth({credential:"Good credential"}, user:{id:"Good id", token:"good token"}).to.has("isValid").equals(true);						 
	});
});

Bad Practices That Make Tests Ineffient

Just Reflect The Implementation

//code
class MyClass{
	function calulate(i){
		return (i < 0) ? i * -1 : i
	}
}

//test
describe("MyClass", function(){
	it("shoul do correct things", function(){
		var instance = new MyClass()
		var num = Math.random();
		expect(instance.calulate(num)).to.be((num < 0) ? num * -1 : num)
	})
})

Why it is bad

That test is clearly not useful: it contains an exact copy of the code under test and acts like a checksum. Once the implementation changed, test cases need to be changed as well.

How to fix it

Test on behaviours.

describe("MyClass", function(){
	it("should return absolute value of input", function(){
		var instance = new MyClass()		
		expect(instance.calulate(1)).to.be(1);
		expect(instance.calulate(-1)).to.be(1);
		expect(instance.calulate(0)).to.be(0);
		expect(instance.calulate(-2)).to.be(2);
	})
})

Write code only for tests

class CallBlocker{
	constructor(user){
		this.user = user;
	}
	init(){ // this part make test hard to write
		Server.readConfig();
		let server = Server.getInstance();
		this.calls = server.getCallsForUser(this.user); 
	}
	
	setCalls(calls){//so we make this to make it possible to mock calls
		this.calls = calls;
	}		
}

//test
describe("CallBlocker", function(){
	it("should block malicious calls", function(){		
		var user = new RealUser(user_info);
		var blocker = new CallBlocker(user);
		yield blocker.init();//complex and wasting time on it
		blocker.setCalls(new MockCalls());// see ! I can mock it eventually
		//assertion		
	});
});

Why it is bad

This is just one of those tricks which adding lines to code just for tests. Usualy those tricks do not respect the normal flow of the internal state transitions of objects and can not fix the root cause of low testability of the object design. As well they leak unexpected access to public use.

How to fix it

Redesign the object graph and make it generally testable.

##Test Implementation rather than Interface

class Calulator(){
	vectorAdd(arr1, arr2){ 
		return arr1.map(function(num1, index){ return num1 + arr2[index]; })
	}
	
	add(a, b){
		return a + b;
	}
}

//test
describe("Calulator", function(){
	it("should do scalar add correctly", function(){		
		var cal = new Calulator();
		expect(cal.add(1,1)).to.be(2);
		expect(cal.add(1,2)).to.be(3);
	});
});

Why it is bad

Interfaces are assumed to be stable while implementations are flexible and changing over time. In above example, if we change the implementation to a GPU accelerated one, the add() tests must be deprecated and wasted. Testing on interfaces can make test cases stable as well. But in some cases, the actually implmentation is too complicated to cover by interface tests, then you may consider add test cases for it even in the cost of low flexibility. e.g. :

class Encrytor(){
	complicatedLogic(){
		
	}
	encrypt(input){
		//some pre manipulation
		complicatedLogic();
		//some post manipulation
	}
	decrypt(output){
		//some pre manipulation
		complicatedLogic();
		//some post manipulation
	}
}

//you want to ensure the complicated logic, so you add unit tests like
describe("Encrytor", function(){
	it("complicatedLogic should be correct", function(){		
		var enc = new Encrytor();
		expect(enc.complicatedLogic("input1")).to.be("output1");
		expect(enc.complicatedLogic("input2")).to.be("output2");				
	});
});

How to fix it

Test on interfaces.

describe("Calulator", function(){
	it("should do vector add correctly", function(){		
		var cal = new Calulator();
		expect(cal.vectorAdd([1,1],[1,2])).to.be([2,2]);
		expect(cal.vectorAdd([1,2],[3,4])).to.be([4,6]);
	});
});

Ways to mock dependencies for tests

Before Read

Code examples here are for demonstration only, parts that may detract readers are removed and simplied, e.g. mongodb without collection, yield without co.js, and connection of external services is ignored.

Example

Imagine that we have to build a data access layer which provides interfaces as set(key, value) and get(key) while it . Under the hood, we use redis for fast read, and mongodb for persistent storage.

class DataStore{
	constructor(redis_client, mongodb_client){
		this.redis = redis_client;
		this.mongodb = mongodb_client;
	}
	
	get(key){
		try
		{
			let res = yield this.redis.get(key);
		}catch(e){
			logger.error("failed to get from cache")
		}
		if(res){
			return res;
		}else
		{
			let res = yield this.mongodb.findOne({"key": key});
		}
		return res.value;
	}
	
	set(key, value){
		try{
			yield redis.set(key, value);
		}catch(e){
			logger.error("failed to set cache")
		}		
		yield mongodb.update({"key":key}, {"key":"key", "value": value}, {"upsert": true})
	}
}

What we want to cover and verify

  • The flow is correct, cache first, and then db later.
  • The data with external services is correct, e.g. when I set a key with “foo”, there must be a record “foo” record in cache and db.
  • The strategy is correct, if cache failed, the code still deliver the result to user.
  • The behaviors are correct, user can get the same thing after they set it.

Way to Mock Dependencies

Mocks are used to simulate the behaviour of objects, so that we can:

  • get rip from the real infrastructures, real client, real implementation and real config
  • control the input and output
  • do assertion on it

For external dependencies (external means “scope out of the code you want to test against”), the main way to replace it with something you can easily access, control and measure.

Mocking is one usual way to achieve this.

Cloned Infrastructure

To test it with mocked dependencies, one way is set up a local redis server and mongo server.

describe("DataStore", function(){
	it("should have record in cache and db when set", function(){
		var redis = new Redis({host: "localhost"});
		var mongodb = new MongoDB({host: "localhost"});
		var ds = new DataStore(redis, mongodb);
		yield ds.set("foo", "bar");
				
		expect(redis.get("foo")).to.be("bar");
		expect(mongodb.findOne({"key": "foo")).value).to.be("bar");
	})
});

but you usually have to write a (bash) scirpt to prepare the servers;

#start redis server
redis-server
#start mongodb server
mongod
#reset states
redis-cli -h 127.0.0.1 flushall
mongo 127.0.0.1 removeall
# start tests
mocha .

So using real infrasturcture to test:

  • guaranteed identical behavious
  • hard to get full control, especially for managed cloud services though some service provider and local version for test, like AWS has dyanmodb-local, but it stil needs you some knowledge to operate.
  • it slows down the process as it is running real servers and introduce many noise to the test.

Manual Mocking

As javascript is very flexible and get() set() in redis is easy to implement, so it is not hard to build a mock from scratch. Let’s have a simple test case first, by which we want to verify that cache is set.


class MockRedis{
	constructor(){ this.map = {}; }
	set(key, value){ this.map[key] = value;}
	get(key){ return this.map[key];}	
}

describe("DataStore", function(){
	it("should have record in cache when set", function(){
		var mocked_redis = new MockRedis(); var mocked_mongodb = new MockedMongodb();
		var ds = new DataStore(mocked_redis, mocked_mongodb);
		yield ds.set("foo", "bar");
		expect(mocked_redis.get("foo")).to.be("bar");
	})
});

Manual mocking:

  • fast, simple, fully controlled
  • only good for very simple behavious and verification
  • need many facilities to do accurate assertion, e.g. if we cares about how many times the set() is called with same args, we need to have a counter and non-trivial counting logic.

Fake Implementation

For famous services, there are usually some useful fake(or fully mocked) implementation in npm.

For example, fakeredis for redis, and mongo-mock for mongodb.

describe("DataStore", function(){
	it("should have record in cache and db when set", function(){
		var redis = new FakeRedis();
		var mongodb = new MongoDBMock();
		
		var ds = new DataStore(redis, mongodb);
		yield ds.set("foo", "bar");
						
		expect(redis.get("foo")).to.be("bar");		
		expect(mongodb.findOne({"key": "foo")).value).to.be("bar");
	})
});

Fakes:

  • Handy to use
  • no real in
  • usually simulate a large part the behavious
  • but sometimes can not cover all features, like fakeredis do not support lua scripting and pubsub.
  • verification usually involve knowledges of the dependencies.

Spy and Stub

Let’s step back a little bit from the problem scope. Why we need to do assertion on the mocked dependencies? We just want to make sure our code does the correct interaction with external services and the effects of interaction can be somehow measured on the services.

Specifically, We can get the value with the key from cache is the expected consequence of set cache with the key and value. The latter thing is what we want to ensure, not the former part. So if we follow the minimalism principal of testing to strip out noise and unexpected efforts of test, we should care about the interaction only, which means, we only verify that our code calls the interfaces with expected input.

That’s the reason why there are Spy and Stub. Essentially they are mocks, but with different features and uses. They simulate objects and help tests to verify interactions.

Here we use sinon.js to illustrate.

describe("DataStore", function(){
	it("should have record in cache and db when set", function(){
		var redis = {
			set: sinon.spy()			
		};
		
		var mongodb = {
			update: sinon.spy()
		};
		
		var ds = new DataStore(redis, mongodb);
		yield ds.set("foo", "bar");
		
		
		expect(redis.set.calledWithArgs("foo","bar")).to.be.ok;		
		expect(mongodb.update.calledWithArgs({"key": "foo"}, {"key":"foo", "value": "bar"},{"upsert": true})).to.be.ok;
	})
});

Actually, we have an other behavior to test: continue to work even the redis cache fails.

Here comes Stub.

describe("DataStore", function(){
	it("should continue to get result from db even cache fails", function(){
		var redis = {
			set: sinon.stub().throws(), // which throws en error once called 
			get: sinon.stub().throws()
		};
			
		var mongodb = {
			update: sinon.spy(),
			findOne: sinon.spy()
		};
		
		var ds = new DataStore(redis, mongodb);
		
		yield ds.set("foo", "bar");						
		expect(mongodb.update.calledWithArgs({"key": "foo"}, {"key":"foo", "value": "bar"},{"upsert": true})).to.be.ok;
		
		yield ds.get("foo");						
		expect(mongodb.findOne.calledWithArgs({"key": "foo"})).to.be.ok;			
	})
});

Or in a more functional test way:

describe("DataStore", function(){
	it("should continue to get result from db even cache fails", function(){
		var redis = { 
			set: sinon.stub().throws(), // which throws en error once called 
			get: sinon.stub().throws()
		};
					
		var mongodb = require("mongo-mock");		
		var ds = new DataStore(redis, mongodb);
				
		yield ds.set("foo", "bar");										
		var res = yield ds.get("foo");
		
		expect(res).to.be("bar"); //even redis fails, I can get the result back						
					
	})
});

Stub can be helpful to test with complex interaction and states, as it gives you more control of the object, e.g.:

describe("DataStore", function(){
	it("should have higher priority for cache", function(){		
		
		var redis = { 
			set: function(){} ,
			get: sinon.stub().onCall("foo").returns("not bar")
		};
					
		var mongodb = require("mongo-mock");		
		var ds = new DataStore(redis, mongodb);
				
		yield ds.set("foo", "bar");										
		var res = yield ds.get("foo");
		
		expect(res).to.be("not bar"); 						
					
	})
});

There are some other goodies from Sinon.js for mocking and assertion, please check http://sinonjs.org/docs/

Let’s have a full test with Sinon.js.

describe("DataStore", function(){
	it("set(key, value)", function(){
				
		it("should put records to cache and db", function(){
			
			var redis = {
				set: sinon.spy()			
			};
		
			var mongodb = {
				update: sinon.spy()
			};
			var ds = new DataStore(redis, mongodb);
			yield ds.set("foo", "bar");
			
			expect(redis.set.calledWithArgs("foo","bar")).to.be.ok;		
			expect(mongodb.update.calledWithArgs({"key": "foo"}, {"key":"foo", "value": "bar"},{"upsert": true})).to.be.ok;
		})
		
		it("should put cache record first and then db record", function(){
			var redis = {
				set: sinon.spy()			
			};
		
			var mongodb = {
				update: sinon.spy()
			};
			var ds = new DataStore(redis, mongodb);
			yield ds.set("foo", "bar");
			
			redis.set.calledBefore(mongodb.update)
		})
		
		it("should put cache record to db record even cache fails", function(){
			var redis = {
				set: sinon.stub().throws()			
			};
		
			var mongodb = {
				update: sinon.spy()
			};
			
			expect(mongodb.update.calledWithArgs({"key": "foo"}, {"key":"foo", "value": "bar"},{"upsert": true})).to.be.ok;
			
		})									
		
	})
	
	it("get(key)", function(){						
		
		it("should not touch db if key exists in cache", function(){
			
			var redis = {
				get: sinon.stub().onCall("foo").returns("bar");			
			};
		
			var mongodb = {
				findOne: sinon.spy()
			};
			
			var ds = new DataStore(redis, mongodb);
		    yield ds.get("foo");
			
			expect(mongodb.findOne.called).to.be.false;
		})
		
		it("should get from db if key does not exist in cache", function(){
			
			var redis = {
				get: sinon.stub().onCall("foo").returns(null);			
			};
		
			var mongodb = {
				findOne: sinon.spy()
			};
			
			var ds = new DataStore(redis, mongodb);
		    yield ds.get("foo");
			
			expect(mongodb.findOne.called).to.be.true;
		})
		
		it("should get from db even cache fails", function(){
			
			var redis = {
				get: sinon.stub().throws();			
			};
		
			var mongodb = {
				findOne: sinon.spy()
			};
			
			var ds = new DataStore(redis, mongodb);
						
		    yield ds.get("foo");
			
			expect(mongodb.findOne.called).to.be.true;
		})								
		
	})
});

Misc

Other than approaches mentioned above, there are many tools helpful in context of node.js and javascript.

Mockery

Dependency Injection is always the good to make it easy to swap dependencies for flexible and testable design. But sometimes we just want some handy helper to swap the modules we required so that dependencies can be injected without extra efforts to arrange code.

Mockey is a library for that purpose, it intercepts all require calls and set up mocked dependencies for you.

If we have such:

var fs = require('fs');
var path = require('path');
class FileLogger{
	getLogPath(user_input_path){
		var stat = yield fs.stat(user_input_path);
		if(stat.isDirectory()){
			return path.join(user_input_path,"server.log")
		}else{
			return path;
		}
	}
}

So the test can be:

describe("FileChecker", function(){
	it("should return a path with 'server.log' if user input is a directory", function(){		
		var fsMock = {
    		stat: function (path) { 
				return {
					isDirectory:function(){return false;}
				};
			 }
		};
		mockery.registerMock('fs', fsMock);
		mockery.enable();
		var logger = new FileLogger();
		var res = logger.getLogPath("/foo/bar");
		
		expect(res).to.be("/foo/bar/server.log");
		mockery.disable();		
	})
});

Mocky, Nock

Specially for external services with REST API, there are some handy tools for testing with controlled response.

Mocky

http://www.mocky.io/ gives you fixed response with static url.

Nock

Nock intercepts runtime just like Mockery, but dedicated for http calls.

describe("ServiceClient", function(){
	it("should retry 5 times if server returns retryable flag", function(){		
		nock('http://myapp.com')
                .get('/')
                .reply(500, {
                  	retryable: true
                });
				
		var client = new ServiceClient();
		var counter = 0;
		client.on('retry', function(){
			counter++;
		});
		
		try{
			yield client.connect();
		}catch(e){}
		
		expect(counter).to.be(5);
	})
});

Conclusion

  • Rule #1: your code should be test friendly, otherwise all tools can not be easily applied to it.
  • Test what you really and only care.
  • Pick the rigth tool and technique to help you, every tool or approach has its good and bad.
  • Balance code quality and effort for testing.