I'm trying to write some unit tests for an Apps Script add-on designed for Google Docs. A few of the functions I'd like to have unit tests for call PropertiesService.getDocumentProperties(). A simple example function in my add-on:
function baseFontSize() {
var baseFontSize = JSON.parse(
PropertiesService.getDocumentProperties().getProperty('baseFontSize'));
if (baseFontSize === null) {
baseFontSize = JSON.parse(
PropertiesService.getUserProperties().getProperty('baseFontSize'));
if (baseFontSize === null) {
PropertiesService.getUserProperties().setProperty('baseFontSize', '11');
baseFontSize = 11;
}
PropertiesService.getDocumentProperties()
.setProperty('baseFontSize', JSON.stringify(baseFontSize));
}
return baseFontSize;
}
I'm writing my tests with the QUnit for Google Apps Script library:
function doGet(e) {
QUnit.urlParams(e.parameter);
QUnit.config({title: 'My test suite'});
QUnit.load(testSuite);
return QUnit.getHtml();
}
QUnit.helpers(this);
function testSuite() {
// some module() and test() calls deleted for brevity...
test('baseFontSize', function() {
// PropertiesService.getDocumentProperties() === null
// how to test baseFontSize()?
});
// more module() and test() calls...
}
Since the test suite is not running within a document, there are no document properties. It seems that the only way to test my function would be to mock the getDocumentProperties function. Of course, the only Apps Script mock/stub libraries I can find are either meant to test within a Node.js environment, or are not sufficiently complete for my needs, which means I would have to roll my own.
While I still hope to find a more elegant solution, until that occurs I have thrown together a simple stub framework based on the SinonJS API, and I'm injecting the stub into my function to be tested (since I can't actually stub PropertiesService, as it's frozen).
Roughly, my solution (my project already includes Underscore, so I take advantage of it where I can):
function stub(object, property, impl) {
const original = object[property];
if (_.isFunction(impl)) object[property] = wrapFunction(impl);
else object[property] = wrapFunction(function() { return impl; });
object[property].restore = function() {
if (!_.isUndefined(original)) object[property] = original;
else delete object[property];
};
return object[property];
}
function wrapFunction(fn) {
const properties = _.mapObject({
get callCount() { return calls.length; },
get called() { return calls.length > 0; },
get notCalled() { return calls.length === 0; },
// etc...
}, function(v, k, o) { return Object.getOwnPropertyDescriptor(o, k); });
const calls = [];
const wrappedFn = function() {
const args = Array.prototype.slice.call(arguments);
var error;
var returnVal;
try { returnVal = fn.apply(this, args); }
catch (e) { error = e; }
calls.push({
thisObj: this,
params: args,
error: error,
returnVal: returnVal,
});
return returnVal;
};
Object.defineProperties(wrappedFn, properties);
return wrappedFn;
}
Then, my test:
test('baseFontSize', function() {
const propService = {};
stub(propService, 'getDocumentProperties', fakeGetProperties());
stub(propService, 'getUserProperties', fakeGetProperties());
equal(baseFontSize(propService), 11);
});
//...
function fakeGetProperties() {
const map = {};
const container = {
deleteAllProperties: function() {_.each(map, function(v, k){depete map[k];});},
deleteProperty: function(k) { delete map[k]; },
getKeys: function() { return Object.keys(map); },
getProperties: function() { return _.clone(map); },
getProperty: function(key) { return map[key] || null; },
setProperties: function(obj, deleteOthers) {
if (deleteOthers) container.deleteAllProerties();
_.each(obj, function(v, k) { map[k] = v; });
},
setProperty: function(key, value) { map[key] = value; },
};
return function() { return container; };
}
And the edited version of my function to test, with DI for PropertiesService:
function baseFontSize(propService) {
if (_.isUndefined(propService)) {
propService = PropertiesService;
}
var baseFontSize = JSON.parse(
propService.getDocumentProperties().getProperty('baseFontSize'));
if (baseFontSize === null) {
baseFontSize = JSON.parse(
propService.getUserProperties().getProperty('baseFontSize'));
if (baseFontSize === null) {
propService.getUserProperties().setProperty('baseFontSize', '11');
baseFontSize = 11;
}
propService.getDocumentProperties()
.setProperty('baseFontSize', JSON.stringify(baseFontSize));
}
return baseFontSize;
}
Related
I want to start writing unit test to my flutter app that have developed using GetX pattern. In GetX pattern, the code is separated to controller and view, so all methods that I want to test is in controller.
In my app, I am using firebase to make authentication with mobile number.
This is LoginController:
class LoginController extends GetxService {
...
LoginController(this.authService);
final _auth = FirebaseAuth.instance;
String validatePhoneNumber(String phoneNumber) {
if (!phoneNumber.startsWith('+20')) {
return 'أدخل كود البلد مثل: +20 في مصر';
} else if (phoneNumber.length < 11) {
return 'أدخل رقم صحيح';
} else if (phoneNumber.isEmpty) {
return 'أدخل رقم الهاتف';
}
return null;
}
Future<void> validate() async {
await _auth.verifyPhoneNumber(
phoneNumber: phoneNumberController.text,
timeout: timeoutDuration,
codeAutoRetrievalTimeout: (String verificationId) {
verId.value = verificationId;
currentState.value = SignInPhoneWidgetState.CodeAutoRetrievalTimeout;
},
codeSent: (String verificationId, [int forceResendingToken]) {
verId.value = verificationId;
currentState.value = SignInPhoneWidgetState.CodeSent;
DialogService.to.stopLoading();
Get.toNamed(Routes.ACCEPT_SMS);
},
verificationCompleted: (AuthCredential phoneAuthCredential) async {
currentState.value = SignInPhoneWidgetState.Complete;
try {
if (authService.currentUser.value != null) {
await authService.currentUser.value
.linkWithCredential(phoneAuthCredential);
} else {
await _auth.signInWithCredential(phoneAuthCredential);
}
Get.offAllNamed(Routes.ROOTHOME);
//widget.onLoggedIn(authResult);
} on PlatformException catch (e) {
print(e);
errorCode.value = e.code;
errorMessage.value = e.message;
} catch (e) {
print(e);
} finally {
//.......
}
},
verificationFailed: (FirebaseAuthException error) {
errorCode.value = error.code;
errorMessage.value = error.message;
currentState.value = SignInPhoneWidgetState.Failed;
},
//forceResendingToken:
);
}
Future<void> validateSmsCode() async {
//_auth.
var cred = PhoneAuthProvider.credential(
verificationId: verId.value, smsCode: verifyCodeController.text);
try {
if (authService.currentUser.value != null) {
await authService.currentUser.value.linkWithCredential(cred);
} else {
await _auth.signInWithCredential(cred);
await Get.offAllNamed(Routes.ROOTHOME);
}
} on PlatformException catch (ex) {
errorCode.value = ex.code;
errorMessage.value = ex.message;
currentState.value = SignInPhoneWidgetState.Failed;
} on FirebaseAuthException catch (ex) {
errorCode.value = ex.code;
errorMessage.value = ex.message;
currentState.value = SignInPhoneWidgetState.Failed;
}
}
...
}
This is login_test.dart file:
I should mock every outside operation like firebase. But In this case I want to test validatePhoneNumber method, that checks if the phone number is valid or not. the method it self hasn't firebase operations. But, the method is called by a LoginController object, And this object it self has instance of FirebaseAuth.instance.
final _authSerive = AuthService();
main() async {
final loginController = LoginController(_authSerive);
setUp(() async {});
tearDown(() {});
group('Phone Validation', () {
test('Valid Email', () {
String result = loginController.validatePhoneNumber('+201001234567');
expect(result, null);
});
});
}
When I tried to run this test method, This error appeared.
Failed to load "D:\bdaya\ta7t-elbeet-client\test\login_test.dart":
[core/no-app] No Firebase App '[DEFAULT]' has been created - call
Firebase.initializeApp()
The reasen is:
This line in The LoginController:
final _auth = FirebaseAuth.instance;
I certainly know that I have to mock Firebase operations.
How to mock it in this case or, What should I do?
I have an Ember app with ember-computed-decorators and I have this kind of model :
import DS from 'ember-data';
import {alias} from 'ember-computed-decorators';
export default DS.Model.extend({
#alias('customData.email') email
});
It worked with ember-cli-babel version 5 but I updated to the version 6 with tranform-decorator-legacy and I have this error :
email is not defined
I reproduced it with a simple js script like this :
function dec(target, name, descriptor) {
const method = descriptor.value;
descriptor.value = function(...args) {
return 'hello';
}
}
const Foo = {
#dec test
}
console.log(Foo.test);
And I have the same error.
This works :
function dec(target, name, descriptor) {
const method = descriptor.value;
descriptor.value = function(...args) {
return 'hello';
}
}
const Foo = {
#dec
test() {
return 'test';
}
}
console.log(Foo.test());
I think #dec test is strange but it worked with babel 5. What's the solution ?
Edit
Here is what's generated by ember :
define('tiny/models/subscription', ['exports', 'ember-data', 'ember-computed-decorators'], function (exports, _emberData, _emberComputedDecorators) {
function _createDecoratedObject(descriptors) { var target = {}; for (var i = 0; i < descriptors.length; i++) { var descriptor = descriptors[i]; var decorators = descriptor.decorators; var key = descriptor.key; delete descriptor.key; delete descriptor.decorators; descriptor.enumerable = true; descriptor.configurable = true; if ('value' in descriptor || descriptor.initializer) descriptor.writable = true; if (decorators) { for (var f = 0; f < decorators.length; f++) { var decorator = decorators[f]; if (typeof decorator === 'function') { descriptor = decorator(target, key, descriptor) || descriptor; } else { throw new TypeError('The decorator for method ' + descriptor.key + ' is of the invalid type ' + typeof decorator); } } } if (descriptor.initializer) { descriptor.value = descriptor.initializer.call(target); } Object.defineProperty(target, key, descriptor); } return target; }
exports['default'] = _emberData['default'].Model.extend(_createDecoratedObject([{
key: 'mail',
initializer: function initializer() {
return _emberData['default'].attr();
}
}, {
key: 'email',
decorators: [(0, _emberComputedDecorators.alias)('mail')],
initializer: function initializer() {
return email;
}
}]));
});
Here is what's generated by ember-cli-babel version 6 :
define('tiny/models/subscription', ['exports', 'ember-data', 'ember-computed-decorators'], function (exports, _emberData, _emberComputedDecorators) {
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
var desc = {};
Object['ke' + 'ys'](descriptor).forEach(function (key) {
desc[key] = descriptor[key];
});
desc.enumerable = !!desc.enumerable;
desc.configurable = !!desc.configurable;
if ('value' in desc || desc.initializer) {
desc.writable = true;
}
desc = decorators.slice().reverse().reduce(function (desc, decorator) {
return decorator(target, property, desc) || desc;
}, desc);
if (context && desc.initializer !== void 0) {
desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
desc.initializer = undefined;
}
if (desc.initializer === void 0) {
Object['define' + 'Property'](target, property, desc);
desc = null;
}
return desc;
}
var _dec, _desc, _value, _obj, _init;
exports.default = _emberData.default.Model.extend((_dec = (0, _emberComputedDecorators.alias)('mail'), (_obj = { email: email
}, (_applyDecoratedDescriptor(_obj, 'email', [_dec], (_init = Object.getOwnPropertyDescriptor(_obj, 'email'), _init = _init ? _init.value : undefined, {
enumerable: true,
configurable: true,
writable: true,
initializer: function initializer() {
return _init;
}
}), _obj)), _obj)));
});
I have the same result with babel 5.
let the service be
function testService(){
function test(){
return{
find:find,
save:save
};
function find(){
//return a promise
}
function save(){
//some code
}
}
return {test:test};
}
let the controller be
function TestController(testService){
var ctrl=this;
ctrl.first=first;
function first(){
testService.test().find().then(function(response){});
}
}
how can i mock the the service call in the 'first' function in controller using jasmine
Something like this...
setup
const testSpy = jasmine.createSpyObj('testService.test', ['find', 'save'])
const testServiceSpy = jasmine.createSpyObj('testService', ['test'])
const reponse = {
status: 200
}
testServiceSpy.test.and.returnValue(testSpy)
testSpy.find.and.returnValue(Promise.resolve(response))
and in your test
const testController = new TestController(testServiceSpy)
testController.first()
expect(testServiceSpy.test).toHaveBeenCalled()
expect(test.find).toHaveBeenCalled()
spyOn(testService, 'test').and.callFake(function () {
return { find: function () {
return {
then: function (callback) {
return callback({ 'status': 3 }); }
};
}
};
});
ctrl.first();
expect(testService.test).toHaveBeenCalled();
i used this method and found working
I'm trying to make an AJAX call to my API over Ember Data (1.0.0 Beta 4), but I don't know how to access the model outside the router. The documentation provides such examples only:
App.PostRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('post', params.post_id);
}
});
My code:
var AuthManager = Ember.Object.extend({
authenticate: function(accessToken, userId) {
var user = this.store.find('user', userId);
/* ... */
},
/* ... */
});
Now I get has no method 'find':
Uncaught TypeError: Object function () {
if (!wasApplied) {
Class.proto(); // prepare prototype...
}
o_defineProperty(this, GUID_KEY, undefinedDescriptor);
o_defineProperty(this, '_super', undefinedDescriptor);
var m = meta(this), proto = m.proto;
m.proto = this;
if (initMixins) {
// capture locally so we can clear the closed over variable
var mixins = initMixins;
initMixins = null;
this.reopen.apply(this, mixins);
}
if (initProperties) {
// capture locally so we can clear the closed over variable
var props = initProperties;
initProperties = null;
var concatenatedProperties = this.concatenatedProperties;
for (var i = 0, l = props.length; i < l; i++) {
var properties = props[i];
Ember.assert("Ember.Object.create no longer supports mixing in other definitions, use createWithMixins instead.", !(properties instanceof Ember.Mixin));
if (typeof properties !== 'object' && properties !== undefined) {
throw new Ember.Error("Ember.Object.create only accepts objects.");
}
if (!properties) { continue; }
var keyNames = Ember.keys(properties);
for (var j = 0, ll = keyNames.length; j < ll; j++) {
var keyName = keyNames[j];
if (!properties.hasOwnProperty(keyName)) { continue; }
var value = properties[keyName],
IS_BINDING = Ember.IS_BINDING;
if (IS_BINDING.test(keyName)) {
var bindings = m.bindings;
if (!bindings) {
bindings = m.bindings = {};
} else if (!m.hasOwnProperty('bindings')) {
bindings = m.bindings = o_create(m.bindings);
}
bindings[keyName] = value;
}
var desc = m.descs[keyName];
Ember.assert("Ember.Object.create no longer supports defining computed properties.", !(value instanceof Ember.ComputedProperty));
Ember.assert("Ember.Object.create no longer supports defining methods that call _super.", !(typeof value === 'function' && value.toString().indexOf('._super') !== -1));
Ember.assert("`actions` must be provided at extend time, not at create time, when Ember.ActionHandler is used (i.e. views, controllers & routes).", !((keyName === 'actions') && Ember.ActionHandler.detect(this)));
if (concatenatedProperties && indexOf(concatenatedProperties, keyName) >= 0) {
var baseValue = this[keyName];
if (baseValue) {
if ('function' === typeof baseValue.concat) {
value = baseValue.concat(value);
} else {
value = Ember.makeArray(baseValue).concat(value);
}
} else {
value = Ember.makeArray(value);
}
}
if (desc) {
desc.set(this, keyName, value);
} else {
if (typeof this.setUnknownProperty === 'function' && !(keyName in this)) {
this.setUnknownProperty(keyName, value);
} else if (MANDATORY_SETTER) {
Ember.defineProperty(this, keyName, null, value); // setup mandatory setter
} else {
this[keyName] = value;
}
}
}
}
}
finishPartial(this, m);
this.init.apply(this, arguments);
m.proto = proto;
finishChains(this);
sendEvent(this, "init");
} has no method 'find'
In Ember Data < 0.14 methods like App.User.find(id) were present but it's deprecated
You can use the dependency injection to inject a store in the AuthManager:
Ember.Application.initializer({
name: "inject store in auth manager",
initialize: function(container, application) {
// register the AuthManager in the container
container.register('authManager:main', App.AuthManager);
// inject the store in the AuthManager
container.injection('authManager', 'store', 'store:main');
// inject the AuthManager in the route
container.injection('route', 'authManager', 'authManager:main');
// inject in the controller
// container.injection('controller', 'authManager', 'authManager:main');
}
});
And in the route you will able to do:
App.IndexRoute = Ember.Route.extend({
model: function() {
this.authManager.authenticate('token', 'userId');
return [];
}
});
See this in action http://jsfiddle.net/marciojunior/3dYnG/
How can I test a service which depends on another service. I currently get Service1Provider not found error in this implementation. How can I properly inject Service1 so I can unit test Service2? Thanks for any tips or tricks.
jsfiddle
gist
!function(ng){
'use strict';
var module = ng.module('foo.services', []);
(function($ng, $module) {
function Service($q) {
return {
bar: function(a,b,c){
var baz = a+b+c;
return function(d,e,f){
var deferred = $q.defer();
if(baz > 0){
deferred.resolve({result: baz + d + e + f });
} else {
deferred.reject({ err: 'baz was <= 0'})
}
return deferred.promise;
}
}
};
}
$module.factory("Service1", ['$q', Service]);
})(ng, module);
(function($ng, $module) {
function Service(Service1) {
function doSomething(){
var result;
var whatever = Service1.bar(5,6,7);
var promise = whatever(8,9,10);
promise.then(function(data){
result = data.result;
//data.result should be 45 here
}, function(err){
});
return result;
}
return {
bam:doSomething
};
}
$module.factory("Service2", ["Service1", Service]);
})(ng, module);
}(angular);
var myApp = angular.module('myApp',['foo.services']);
If you are just testing Service2 then you should try to eliminate any dependency on Service1 in the test. Your test could have something like:
module('foo.services', function($provide) {
$provide.value('Service1', MockService1());
});
This will give the return value from the function MockService1 instead of actually using the service, whenever it is injected.
Then you have MockService1 function return a skeleton of the actual service with the same function. In your test you can then wait for the promise to be resolved by doing something like this:
bar: function(...) {
var def = $q.defer();
$timeout(function() {
def.resolve('test');
});
return def.promise;
}
// This is in your test
bar.then( /* .... some tests */ );
// This executes the timeout and therefor the resolve
$rootScope.$digest();
$timeout.flush();
Hope this helped