I am writing a unit test using mocks and would like to ask an opinion. My specific example is mocking the "window" object, however the problem could occur in other scenarios.
To note, I am reverse engineering the unit tests, TDD is a new style which we are adopting here.
The module I am testing is one that will return the hash part of the URL and grab parameters from it. For example if the hash value was:
#param=value¶m2=value2
Then the module would return an object like:
{
param: 'value',
param2: 'value2'
}
The first unit test I am writing is one which returns the hash value without the hash. Here is the method:
getCurrentHash: function() {
return window.location.hash.slice(1);
},
I have mocked the window object to set window.location.hash equal to #param=value¶m2=value2
I am expecting getCurrentHash to return param=value¶m2=value2 in this particular instance.
The window object is a dependency on the hash module, I have switched the path when running the tests to use the mocked version.
The Question
Should unit test explicitly test against the mocked data like this:
tdd.test('getCurrentHash', function () {
var currentHash = hash.getCurrentHash();
currentHash.should.equal('param=value¶m2=value2');
});
Or should the unit test be testing the format of the return value, like this:
tdd.test('getCurrentHash', function () {
var currentHash = hash.getCurrentHash();
currentHash.should.not.contain('#');
currentHash.should.be.a('string');
currentHash.should.have.length.above(0);
});
This question came about due to the fact that the mock might change. If for example we decided to add spaces into our mock we would end up amending it to param=value¶m2=value2+withspace. This would then make the test fail and I would need to change the test to:
tdd.test('getCurrentHash', function () {
var currentHash = hash.getCurrentHash();
currentHash.should.equal('param=value¶m2=value2+withspace');
});
However, in the case of the second test which is more generic, I would not need to change the unit test.
I have read a few articles including the ones listed below, but I couldn't find any detailed reasoning on what an assertion should assert
http://martinfowler.com/articles/mocksArentStubs.html
Unit Tests: How to Assert? Asserting results returned or that a method was called on a mock?
you want to test the behaviour you want to see from the thing you are testing. Your behaviour is (based on the implementation) that the window locations hash value minus the '#' char is returned. So you should test things like:
when no has values exist, nothing is returned (but no error is returned (unless you explicitly want that))
The expected value is returned when a value exists
your second test has little value IMHO as I could change the implementation to this and the tests would still pass.
getCurrentHash: function() {
return "bob";
}
This makes the test fairly useless. your tests exist to verify the behaviour of the system. So write tests that set up happy path conditions, failure cases and edge cases and have them verify that things behave in the way you expect.
Related
How can I monkey-patch some methods in the global jest object for all test files at once? I don't want to add any extra code to my test files, it has to be done somewhere in setup and it can be an ugly hack.
I tried doing that from a custom environment, setupFiles and setupFilesAfterEnv, but it looks like they all get a different instance of jest object and my changes aren't visible in test files.
Disclaimer: I know that it's a bad thing to do but I need it for some one-time benchmarking only and it's the easiest solution that gets the job done.
I got this working! You're right, Jest does re-construct the global jest object for every test case, but you can override the function it uses to do that. In jest.config.js, set globalSetup to something like <rootDir>/jest-global-setup.js. Then, in jest-global-setup.js, add this:
const jestRuntime = require('jest-runtime');
const { _createJestObjectFor } = jestRuntime.prototype;
jestRuntime.prototype._createJestObjectFor = function(...args) {
// Call the original function to create a normal jest object.
const jestObject = _createJestObjectFor.apply(this, args);
// Apply your changes.
jestObject.isMonkeyPatched = true;
// Return the patched object.
return jestObject;
}
// Jest expects to find a function of some sort as well,
// but we don't need it for this example.
module.exports = function() { /* do nothing */ }
I am new for unit test and TDD method so could you help me please.
function calculate(a, b) {
var sum = a + b;
var sub = a - b;
return { sum: sum, sub: sub };
}
With jest you can do it like this :
describe('calculation', ()=> {
let result
beforEach(()=>{
result = calculate(2, 3)
})
it('returns the correct sum', (){
expect(result.sum).toBe(5)
})
it('returns the correct sub', (){
expect(result.sub).toBe(-1)
})
})
it('checks calculations', function() {
var calculate = require('./example.js');
expect(calculate(1, -1).sum).toBe(0);
expect(calculate(1, -1).sub).toBe(2);
});
Save the file as a .js and use npm test.
I am assuming that you are using jestjs.
You can definitely add other test conditions depending upon your understanding of the function.
A very different answer here: the huge problem with testing such code manually is the fact that it is hard to tell when you wrote enough tests covering different cases.
An alternative approach here: instead of specifying only expected results you step back and identify the contracts that your method under test should adhere to.
And then you use one of the quickcheck-based frameworks, such as JSVerify. You give those rules to the tool - and then the tool creates random data and runs test cases. And it case it finds violations of the contract, it will then try to "minimize" the test input - so that you, in the end receive a message "when you use this data x,y,z then rule A is violated".
It is a very different approach compared to "normal" TDD and unit testing, but especially for such kind of functionality it can be a very effective additional thing to do.
In my logging helper class, I have the following:
this.myInfo = console.info.bind(console);
When I call my myInfo function from elsewhere, the calling object and line number are correctly retained and logged in the Chrome devtools.
When I run myInfo though, I also want to run another local function in addition to the console.info. Hence, I figured I could just wrap the above and it would work. I've come up with the following:
var obj = this;
this.myInfo = (function() {
console.info.apply(this, arguments);
myOtherFunc.apply(obj, arguments);
}).bind(console);
The problem is that unlike my first example, I lose the calling context for console.info, and the wrong line number and file are logged in the devTools.
How can I wrap the first example and retain the proper context for the console.info?
You can use getter. In getter you call your other function and then return console.info.bind(console) to caller.
Object.defineProperty(this, "myInfo", { get: function () {
myOtherFunc();
return console.info.bind(console);
}});
In case of passing arguments. You can define following function:
this.myInfo = function()
{
myOtherFunc.apply(null, arguments);
return console.bind.apply(console, arguments);
}
// example of call
this.myInfo(1,2,3)();
I've new solution. You can implement your console.log wrapper in separate JS file or evaluate it with sourceURL then go to Chrome DevTools settings and add "console-wrapper.js" url to blackbox pattern or blackbox this script by link when first message is arrived to console.
When script become blackboxed then all messages will have correct location in source code.
It works in last Google Chrome Canary build and will be available in stable in around two months.
eval("\
function myAwesomeConsoleLogWrapper() {\
console.log.call(console, arguments);\
makeAnotherWork();\
}\
//# sourceURL=console-wrapper.js");
Alexey Kozyatinskiy's approach is cool. However, if not-pretty code like this.myInfo(1,2,3)() is a more serious problem than ugly console output, you could use the wrapper you posted in your question and print needed filename and line number manually having it extracted from new Error().stack. I'd personnaly use Alexey's method unless there was a team working on this project.
The doc for yeoman unit testing seems to be oriented around integration testing, namely running the entire generator and then examining the side effects produced i.e. for the existence of certain files. For this you can use helpers.run().
This is all fine and well, but I also want to be able to unit test a single method (or "priority") and test internal states of the generator i.e. internal vars. I have been able to do this before by using createGenerator like so:
subAngularGenerator = helpers.createGenerator('webvr-decorator:sub-angular', [
path.join(__dirname, '../generators/sub-angular')
],
null,
{'artifacts': artifacts, appName: APP_NAME, userNames: userNames,
});
This has no RunContext, but I can usually add enough things to the structure so that it will run. For instance:
// mixin common class
_.extend(subAngularGenerator.prototype, require('../lib/common.js'));
// we need to do this to properly feed in options and args
subAngularGenerator.initializing();
// override the artifacts hash
subAngularGenerator.artifacts = artifacts;
// call method
subAngularGenerator._injectDependencies(fp, 'controller', ['service1', 'service2']);
Which allows me to test internal state:
var fileContents = subAngularGenerator.fs.read(fp);
var regex = /\('MainCtrl', function \(\$scope, service1, service2\)/m;
assert(regex.test(fileContents));
This works fine as long as the method is basic javascript, like for/next loops and such. If the method make use of any 'this' variables, like this.async(), I get 'this.async' is not a function.
initialPrompt: function () {
var prompts = [];
var done = this.async(); //if this weren't needed my ut would work
...
I can manually add a dummy this.async, but then I go down the rabbit's hole with other errors, like 'no store available':
AssertionError: A store parameter is required
at Object.promptSuggestion.prefillQuestions (node_modules/yeoman-generator/lib/util/prompt-suggestion.js:98:3)
at RunContext.Base.prompt (node_modules/yeoman-generator/lib/base.js:218:32)
at RunContext.module.exports.AppBase.extend.prompting.initialPrompt (generators/app/index.js:147:12)
at Context.<anonymous> (test/test-app.js:158:42)
I tried to create a runContext and then add my generator to that:
var helpers = require('yeoman-generator').test;
// p.s. is there a better way to get RunContext?
var RunContext = require('../node_modules/yeoman-generator/lib/test/run-context');
before(function (done) {
appGenerator = helpers.createGenerator('webvr-decorator:app', [
path.join(__dirname, '../generators/app')
],
null,
appName: APP_NAME, userNames: userNames,
{});
app = new RunContext(appGenerator); //add generator to runContext
});
app.Generator.prompting.initialPrompt(); //gets async not defined
But this gets the same problem.
My theory is the problem has to with 'this' contexts. Normally the method runs with the 'this' context of the entire generator (which has a this.async etc), but when I run the method individually, the 'this' context is just that of the method/function itself (which has no async in its context). If this is true, then it's really more of a javascript question, and not a yeoman one.
It seems like there should be an easy way to unit test individual methods that depend on the generator context such as calls to this.async. I referred to generator-node as an example of best practices, but it only appears to be doing integration testing.
Does anyone have any better ideas, or do I need to just keep futzing around with JavaScript techniques?
Many Thanks.
I was able to get it to work, but it's a total hack. I was able to decorate a RunContext with the necessary artifacts, and then using apply, I put my generator in the context of the RunContext:
var appGenerator;
var app;
before(function (done) {
// create a generator
appGenerator = helpers.createGenerator('webvr-decorator:app', [
path.join(__dirname, '../generators/app')
],
null,
appName: APP_NAME, userNames: userNames,
{}
);
// get a RunContext
app = new RunContext(appGenerator);
// the following did *not* work -- prompts were not auto-answered
app.withPrompts({'continue': true, 'artifactsToRename': {'mainCtrl' : 'main'}});
//add the following functions and hashes from the generator to the RunContext
app.prompt = appGenerator.prompt;
app._globalConfig = appGenerator._globalConfig;
app.env = appGenerator.env;
// the following two lines are specific to my app only
app.globals = {};
app.globals.MAIN_CTRL = 'main';
done();
});
it('prompting works', function () {
// Run the generator in the context of RunContext by using js 'call'
appGenerator.prompting.initialPrompt.call(app);
}
I no longer get any 'missing functions' messages, but unfortunately the prompts are not being automatically provided by the unit test, so the method stops waiting for something to feed the prompts.
The big "secret" was to call with apply which you can use to override the default this context. I put the generator in the context of the RunContext, which verifies my theory that the problem is about being in the improper context.
I assume there's a much better way to do this and that I'm totally missing something. But I thought I'd at least document what I had to do to get it to work. In the end, I moved the variable initialization code from the 'prompting'method, into the 'initializing' method, and since my 'intializing' method has no Yeoman runtime dependencies, I was able to use a simple generator without a RunContext. But that was just fortuitous in this case. In the general case, I would still like to find out the proper way to invoke a single method.
Quite a large portion of my work day to day involves working with Dynamics CRM and writing JS to extend the functionality on the forms.
Most clientside interaction in Dynamics involves using an object provided for you when the form loads, which is just Xrm. So you might have something like:
function OnLoad() {
Xrm.Page.getAttribute('name').setValue('Stackoverflow!');
var x = Xrm.Page.getAttribute('name').getValue();
}
I tend to write a wrapper for the Xrm object, mainly because it is a lot easier than remembering some of the chaining and end up with something like:
function WrappedXrm(realXrm) {
var xrm = realXrm;
this.getValue(name) {
return xrm.getAttribute(name).getValue();
}
}
//and then use it as so
var myXrm = new FakeXrm(Xrm);
var myXrmValue = myXrm.getValue('Name');
I am trying out QUnit and wondering how would I go about unit testing in this scenario?
Obviously the example above is a single line, it might not be worth testing it. But assume there was some business logic there that I wanted to test.
The only way I can see is doing some set up before each test along the lines of
var fakeXrm = {};
fakeXrm.Page = {};
fakeXrm.Page.getAttribute = function(name) {
var tempAttr = {};
tempAttr.getValue = function() {
return 'A fake value';
}
}
And then testing on 'A fake value' being returned, but this doesn't 'feel' right to me at all.
Where am I going wrong?
Using Mocks
So in this case, you want to create an instance of WrappedXrm, and pass it an object that mocks the Xrm from your lib ; you need a mock of Xrm.
A first alternative is to write it like you did (which is perfectly valid, if you know what the interface of Xrm is.)
Some libraries like sinon.js or "spies" in the jasmine framework can help you write code like ;
create a 'mock' Xrm, to configure what it should return
create an instance of WrappedXrm with this mock
call the getValue method of WrappedXrm
check that some method was called on the mock
But in the case of javascript, simply created a object that has just the right properties might be okay.
Note that your tests would break if the structure of the "real" Xrm object changes ; that might be what bother's you, but that's always the risk with mocks...
Using the real implementation
If you don't want to test against a mock (which might make sense in case of a wrapper), then maybe you can write the mimimal code that would create an actual Xrm object in your qunit html page (Maybe hardcoding markup ? I don't know the library, so...)
Hoping this helps.