I am contributing to a project which is built with React (with webpack) running in Electron. When executing unit tests with Jest, it fails with the error TypeError: Cannot read property 'on' of undefined (and works fine when not testing, eg. run with Electron).
The code:
import React, { Component } from 'react';
import { ipcRenderer } from 'electron';
// some more imports
class Setup extends Component {
constructor(props) {
super(props);
this.state = {
// some state
};
ipcRenderer.on('open-file-reply', this.someMethod); // << fails on this line
}
// more class stuff
}
It took me a few days but finally, I found this answer in this great blog post. Quote:
Jest is called from Node and doesn't run test code through Webpack.
Instead, we have to use Jest's mocking functions to replace the import
with a stub file.
Jest has a helper method called moduleNameMapper [object<string, string>] . From jest documentation:
A map from regular expressions to module names that allow to stub out
resources, like images or styles with a single module.
It should be added in your package.json root object like this:
{
"name": "My awesome app",
"jest": {
"moduleNameMapper": {
"electron": "<rootDir>/src/components/tests/mock/electron.js"
}
}
}
and the mock file itself (/src/components/tests/mock/electron.js):
export const ipcRenderer = {
on: jest.fn()
};
This way you can stub other electron modules and methods (like remote which is shown in the blog above).
Another way is creating an electron.js file in __mocks__ in your root folder.
The electron.js should look something like
export const ipcRenderer = {
on: jest.fn(),
};
You can read more at https://jestjs.io/docs/en/manual-mocks#mocking-node-modules
I need to be able test my component (methods, computed properties, data, ...). However, when I import my vue component in my unit test:
import Pagination from 'src/components/shared/pagination.vue'
import { newComponent } from '../component-factory'
describe('pagination.vue', () => {
const propsData = {
metadata: {
page: 2,
records: 155,
total: 11,
},
perPage: 15,
}
it('should have correct number of records', () => {
const ctor = Vue.extend(Pagination)
const vm = new ctor({propsData}).$mount()
expect(vm.firstRecord).toBe(16)
expect(vm.lastRecord).toBe(30)
})
...
vm is of type Vue, and thus does not have the firstRecord/lastRecord properties. Running the test with karma shows a success, but the typescript compiler spits out Errors:
ERROR in ./tests/shared/pagination.spec.ts
(16,19): error TS2339: Property 'firstRecord' does not exist on type 'Vue'.
ERROR in ./tests/shared/pagination.spec.ts
(17,19): error TS2339: Property 'lastRecord' does not exist on type 'Vue'.
I tried casting:
...
const vm = new ctor({propsData}).$mount() as Pagination
...
But that results in a warning in VSCode:
[ts] Cannot find name 'Pagination'.
And has the effect of treating vm as type any which is totally counterproductive.
I think this all stems from the fact that when using .vue files you have to add the declaration:
declare module '*.vue' {
import Vue from 'vue'
export default typeof Vue
}
Which clearly sets the type of all .vue files to Vue, which isn't exactly a lie, but isn't helpful either... Any suggestions? What am I doing wrong?
For future reference, I have attempted to use vuetype which generates .d.ts files for each .vue file, but ran into this issue. Also, there is a request to make .vue a first class citizen in the typescript ecosystem, which would eliminate this problem. And, I just added a request for a vue language service extension
Up until Vue 2.5, their TypeScript documentation page recommended exporting an interface that extends Vue if you were not going to use vue-class-component. You can export this interface to use in your tests, to cast your component instance. The recommendation has been removed from the docs, but I have not been able to figure out how to change my tests to not need the interface.
It looks like vuetype could generate these interfaces for you, but I've just been creating them manually.
Here is a greatly simplified example, but you can define anything in your interface that you would reference on vm, ie data, props, methods:
// NOTE: Make sure your interface extends `Vue`!
export interface PaginationComponent extends Vue {
firstRecord: number,
lastRecord: number
}
export default {
name: 'Pagination',
data: function() {
return {
firstRecord: 16,
lastRecord: 30,
}
}
}
For your test, you can cast the component instance to the type of your exported interface:
import Pagination, {PaginationComponent} from 'src/components/shared/pagination.vue'
describe('pagination', () => {
it('should know about component data fields', () => {
const ctor = Vue.extend(Pagination)
const vm : PaginationComponent = new ctor().$mount()
expect(vm.firstRecord).toBe(16)
expect(vm.lastRecord).toBe(30)
})
})
I have the following directive:
function TopLevelMenuDirective ($userDetails, $configuration) {
return {
restrict:'A',
templateUrl: staticFilesUri + 'templates/TopLevelMenu.Template.html',
scope: {
activeTab: '='
},
link: function (scope, element, attributes) {
var userDetails = $userDetails;
if ($userDetails) {
scope.user = {
name: userDetails.name ? userDetails.name : 'KoBoForm User',
avatar: userDetails.gravatar ? userDetails.gravatar: (staticFilesUri + '/img/avatars/example-photo.jpg')
};
} else {
scope.user = {
name: 'KoBoForm User',
avatar: staticFilesUri + '/img/avatars/example-photo.jpg'
}
}
scope.sections = $configuration.sections();
scope.isActive = function (name) {
return name === scope.activeTab ? 'is-active' : '';
}
}
}
}
I want to mock the dependencies to unit test the different code paths with values known by the unit tests. I have the following sample unit test:
it('should set $scope.user to values passed by $userDetails',
inject(function($compile) {
var element = '<div top-level-menu></div>';
element = $compile(element)($scope);
$scope.$apply();
expect(element.isolateScope().user.name).toBe('test name');
expect(element.isolateScope().user.avatar).toBe('test avatar');
}
));
This gives me two problems.
First, since the template is in an external file, when it loads it tries to fetch it and errors out beacause the file is nowhere to be found, which is logical since it's in a test environment and not an actual server.
Second, there's no apparent way to mock the dependencies injected into the directive through its constructor. When testing controllers you can use the $controller service, but since directives are instantiated indirectly by compiling an html tag with a passed scope, there's no way to instantiate it directly (e.g. there's no analogous $directive). This impedes me from setting $userDetails.name and $userDetails.gravatar to 'test name' and 'test avatar' respectively.
How do I get the directive to compile properly and run with a custom $userDetails dependency?
To load the template file you must configure karma-ng-html2js-preprocessor in karma.
First, visit this page and follow the installation instructions. Then, you need to add a couple of entries in your karma.config.js file:
files: [
'templates/*.html'
],
this tells karma to load all html files in the templates folder (if your templates are somewhere else, put that folder there).
preprocessors: { '**/*.html': 'ng-html2js' },
this tells karma to pass all html files through the ng-html2js preprocessor, which then transforms them into angular modules that put the templates into the $templateCache service. This way, when $httpBackend queries the "server" for the template, it get's intercepted by the template cache and the correct html is returned. All fine here, except for the template's URL: it has to match the templateUrl property in the directive, and ng-html2js passes the full path as the uri by default. So we need to transform this value:
ngHtml2JsPreprocessor: {
cacheIdFromPath: function(filepath) {
var matches = /^\/(.+\/)*(.+)\.(.+)$/.exec(filepath);
return 'templates/' + matches[2] + '.' + matches[3];
}
},
this receives filepath and passes it through a regular expression that extracts the path, file name and extension into an array. You then prepend 'templates/ to the file name and extension and you get the expected uri.
After all this is done making the template available is a matter of loading the module before your test is run:
beforeEach(module('templates/TopLevelMenu.Template.html'));
keep in mind, module is an external service located in angular-mocks.js.
for injecting a custom service into the directive you need to override the service's provider:
beforeEach(module(function ($provide) {
$provide.provider('$userDetails', function () {
this.$get = function () {
return {
name: 'test name',
gravatar: 'test avatar'
};
}
});
}));
$provide is the service that provides your providers. So, if you want to inject a mock dependency you override the provider here.
With that code executing before your test you'll have a mock $userDetails service that returns your predefined strings.
I have uses angular translate from here (http://pascalprecht.github.io/angular-translate/) and it's just work fine, but it break my controller's unit test whith Error:
Unexpected request: GET scripts/i18n/locale-en.json
I don't understant why?
I use yeoman and test with karma.
app.js:
'use strict';
(function() {
angular.module('wbApp', ['authService', 'authUserService', 'checkUserDirective', 'ui.bootstrap', 'pascalprecht.translate'])
.config(function($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'views/login.html',
controller: 'LoginCtrl',
access: {
isFree: true
}
})
.when('/main', {
templateUrl: 'views/main.html',
controller: 'MainCtrl',
access: {
isFree: false
}
})
.otherwise({
redirectTo: '/'
});
});
})();
configTranslate.js:
'use strict';
(function() {
angular.module('wbApp')
.config(['$translateProvider',
function($translateProvider) {
$translateProvider.useStaticFilesLoader({
prefix: 'scripts/i18n/locale-',
suffix: '.json'
});
$translateProvider.preferredLanguage('en');
}]);
})();
karma.conf.js:
files = [
...
'app/bower_components/angular-translate/angular-translate.js',
'app/bower_components/angular-translate-loader-static-files/angular-translate-loader-static-files.js',
...
];
controller test:
'use strict';
describe('Controller: LoginCtrl', function() {
// load the controller's module
beforeEach(module('wbApp'));
var LoginCtrl, scope, location, httpMock, authUser;
// Initialize the controller and a mock scope
beforeEach(inject(function($controller, $rootScope, $location, $httpBackend, AuthUser) {
authUser = AuthUser;
location = $location;
httpMock = $httpBackend;
scope = $rootScope.$new();
LoginCtrl = $controller('LoginCtrl', {
$scope: scope
});
httpMock.when('GET', 'scripts/i18n/locale-en.json').passThrough();
}));
it(...);
...
});
if i add this in test controller, product same error:
httpMock.when('GET', 'scripts/i18n/locale-en.json').respond(200);
httpMock.flush();
or
httpMock.when('GET', 'scripts/i18n/locale-en.json').passThrough();
httpMock.flush();
i find this post How do I test controllers with Angular Translate initialized in App Config? but not helped me :/
I extensively use $httpBackend in my tests and it works fine, but in this case it is ineffective. If I comment the line:
$translateProvider.preferredLanguage('en');
obviously an error, if I add on the runtime (in my controllers)
$translate.uses(local);
I end up with the same error?
So I turn to the translation configuration (configTranslate.js) or at runtime is the same result:
Unexpected request: GET scripts/i18n/locale-en.json
Here is the syntax that I tested, either in a "beforeEach(inject(function(...});"
or in a test "it('...', function() {...});"
httpMock.expectGET('scripts/i18n/locale-en.json');
httpMock.when('GET', 'scripts/i18n/locale-en.json').passThrough();
httpMock.when('GET', 'scripts/i18n/locale-en.json').respond(data);
with at end
httpMock.flush();
I also tried a $ apply
httpMock.expectGET('scripts/i18n/locale-fr.json');
scope.$apply(function(){
$translate.uses('fr');
});
httpMock.flush();
nothing happens, Still this error is driving me crazy ..
If you have any suggestion
it's a known issue, please follow the documentation here: unit testing angular
The solution
Unfortunately, this issue is caused by the design of
angular-translate. To get around these errors, all we can do is to
overwrite our module configuration in our test suite, that it doesn't
use asynchronous loader at all. When there's no asynchronous loader,
there's no XHR and therefore no error.
So how do we overwrite our module configuration at runtime for our
test suite? When instantiating an angular module, we can always apply
a inline function which is executed as configuration function. This
configuration function can be used to overwrite the modules
configuration since we have access to all providers.
Using the $provide provider, we can build a custom loader factory,
which should then be used instead of the static files loader.
beforeEach(module('myApp', function ($provide, $translateProvider) {
$provide.factory('customLoader', function () {
// loader logic goes here
});
$translateProvider.useLoader('customLoader');
}));
Please read more in the above link provided.
We took the approach of ignoring the translation loader in unit tests, rather than being forced to modify each of the spec files.
One way to do it could be by separating the loader configuration to a separate file and then exclude it in karma.
So for example you can create a file app-i18n-loader.js (all other module configurations takes place in a different file):
angular
.module('myApp')
.config(loaderConfig);
loaderConfig.$inject = ['$translateProvider', '$translatePartialLoaderProvider'];
function loaderConfig($translateProvider, $translatePartialLoaderProvider) {
$translateProvider.useLoader('$translatePartialLoader', {
urlTemplate: 'assets/i18n/{part}/{lang}.json'
});
$translatePartialLoaderProvider.addPart('myApp');
}
And in your karma.conf.js exclude the file:
files: [
'bower_components/angular/angular.js',
'bower_components/angular-mocks/angular-mocks.js',
//...
'bower_components/angular-translate/angular-translate.js',
'bower_components/angular-translate-loader-partial/angular-translate-loader-partial.js',
'app/**/*.mdl.js',
'app/**/*.js'
],
exclude: [
'app/app-i18n-loader.js'
],
(Note: Answer edited to a solution that does not require grunt/gulp).
I wanted a solution,
which was not too hacky
which didn't require me to change my actual application code,
which wouldn't interfere with the ability to load additional modules
and most importantly which wouldn't require me to change every
single test.
This is what I ended up with:
// you need to load the 3rd party module first
beforeEach(module('pascalprecht.translate'));
// overwrite useStaticFilesLoader to get rid of request to translation file
beforeEach(module(function ($translateProvider) {
$translateProvider.useStaticFilesLoader = function () {
};
}));
Assuming you don't need the actual translations for your unit tests, this works great. Just put the beforeEach on a global level, preferably in it's own file inside the test folder. It will be executed before every other test then.
I encountered this problem with protractor tests. My solution was to mock translations like this:
angular.module('app')
.config(function ($translateProvider) {
$translateProvider.translations('en', {});
$translateProvider.preferredLanguage('en');
})
Now no language files are downloaded, no strings get translated and I just test against the string keys in specifications:
expect(element(by.css('#title')).getText()).toEqual('TITLE_TEXT');
Try putting to test method:
it('should ...', function() {
httpMock.when('GET', 'scripts/i18n/locale-en.json').respond({});
httpMock.expectGET('scripts/i18n/locale-en.json');
scope.resetForm(); // Action which fires a http request
httpMock.flush(); // Flush must be called after the http request
}
See examples from Angular docs
Please have a look at https://github.com/PascalPrecht/angular-translate/blob/master/test/unit/service/loader-static-files.spec.js as a reference.
In general, I would recommend using a standard translation loader for unit tests (without the hassle of http loadings) which means you can provide the labels with $translateProvider.translations(). Why? Because you do not have to test the remote loading functionality which is part of angular-translate project.
None of the solutions worked for me but I came with these solutions:
1) If you need to use scope.$apply(), or should deal with states in your test (after the $apply() the 2nd approach won't work), override your app's translations with the $translateProvider.translations() method, using a plugin to load JSON files
beforeEach(module(function ($translateProvider) {
$translateProvider.translations('en', readJSON('scripts/i18n/locale-en.json'));
}));
2) If your tested controller depends on the $translate service you can use a plugin to load JSON files and combine it with $httpBackend to load your locale file when angular-translate requests it.
beforeEach(inject(function (_$httpBackend_) {
$httpBackend = _$httpBackend_;
$httpBackend.whenGET('scripts/i18n/locale-en.json').respond(readJSON('scripts/i18n/locale-en.json'));
$httpBackend.flush();
})));
Note this should be below your beforeEach(module('myApp')); or you will get an $injector error.
I made a simple mock service for $translate
$translate=function (translation) {
return {
then: function (callback) {
var translated={};
translation.map(function (transl) {
translated[transl]=transl;
});
return callback(translated);
}
}
};
Usage example here : https://gist.github.com/dam1/5858bdcabb89effca457
I use this pattern.
ApplicationModule set regular angular-translate config.
test code load 'testModule' instead of 'applicationModule'
// application module .js
(function() {
'use strict';
angular
.module('applicationModule', [
'ngAnimate',
'ngResource',
'ui.router',
'pascalprecht.translate'
])
.config(['$stateProvider', '$urlRouterProvider', '$translateProvider', '$translatePartialLoaderProvider', config]);
function config($stateProvider, $urlRouterProvider, $translateProvider, $translatePartialLoaderProvider) {
// set routing ...
$translateProvider.useStaticFilesLoader({
prefix: 'i18n/locale-',
suffix: '.json'
});
$translateProvider.useMessageFormatInterpolation();
$translateProvider.fallbackLanguage(['en']);
$translateProvider
.registerAvailableLanguageKeys(['en', 'ko'], {
'en_US': 'en',
'ko_KR': 'ko'
})
.determinePreferredLanguage(navigator.browserLanguage);
$translateProvider.addInterpolation('$translateMessageFormatInterpolation');
$translateProvider.useSanitizeValueStrategy('escaped');
}
})();
// test.module.js
(function() {
'use strict';
angular
.module('testModule', ['applicationModule'])
.config(['$translateProvider', '$translatePartialLoaderProvider', config])
.run(['$httpBackend', run]);
function config($translateProvider, $translatePartialLoaderProvider) {
$translateProvider.useLoader('$translatePartialLoader', {
urlTemplate: 'i18n/locale-en.json'
});
$translatePartialLoaderProvider.addPart('applicationModule');
}
function run($httpBackend) {
$httpBackend.when('GET', 'i18n/locale-en.json').respond(200);
}
})();
// someDirective.spec.js
describe("a3Dashboard", function() {
beforeEach(module("testModule"))
var element, $scope;
beforeEach(inject(function($compile, $rootScope) {
$scope = $rootScope;
element = angular.element("<div>{{2 + 2}}</div>");
$compile(element)($rootScope)
}))
it('should equal 4', function() {
$scope.$digest();
expect(element.html()).toBe("4");
})
})
Late to the table with this, but I got round this by specifying that Karma simply serve the files as per this entry in karma.conf.js:
files: [
...
{pattern: 'scripts/i18n/*.json', included: false, served: true},
...
]
The 2016 answer for this is to preprocess your json into your tests and properly test translations work on your directives.
I use karma-ng-json2js-preprocessor. Follow all the steps to setup your karma.conf then in your test file, prepend the relevant file as a module, then set that information in $translateProvider.
beforeEach(module('myApp', '/l10n/english-translation.json'));
// Mock translations for this template
beforeEach(module(function($translateProvider, englishTranslation) {
$translateProvider.translations('en_us', englishTranslation);
$translateProvider.useSanitizeValueStrategy(null);
$translateProvider.preferredLanguage('en_us');
}));
Note according to the plugin, it uses your filename to generate a camelcased module name. You can play with the function inside the module's /lib but basically it remove all dashes but KEEPS underscores in a camelCase. So en_us becomes En_us.
You'll also need to tell your test that it is expecting that file as a GEt.
$httpBackend.expect('GET', '/l10n/english-translation.json').respond(200);
I'm attempting to unit test controller code inside a module that takes other modules as dependencies, but haven't been able to figure out how to mock them properly.
I'm using the Jasmine Framework and running my tests with Karma (Testacular).
Module Code
var app = angular.module('events', ['af.widgets', 'angular-table']);
app.controller('eventsCtrl', function([dependencies]){
$scope.events = [];
...
});
Spec Code
describe('events module', function(){
var $scope,
ctrl;
beforeEach(function(){
angular.mock.module('af.widgets', []);
angular.mock.module('angular-table', []);
module('events', ['af.widgets', 'angular-table']);
});
beforeEach(inject(function($rootScope, $controller){
$scope = $rootScope.new();
ctrl = $controller('NameCtrl', {
$scope: $scope,
});
}));
it('should have an empty events array', function(){
expect($scope.events).toBe([]);
})
});
The error I'm getting is Karma is "no module af.widgets", so obviously I'm not mocking the module dependencies right. Any hints?
If you want to mock a module that declare one or more services I have used this code:
beforeEach(function(){
module('moduleToMock');
module(function ($provide) {
$provide.value('yourService', serviceMock);
});
});
This is useful if the service you want to mock is also a service that you want to unit test (in another jasmine describe).
The solution proposed by fscof is fine but you cannot create a unit test for the angular-table module.
Here's what I figured out:
I wasn't loading any 'angular-table' modules in my karma.conf.js file, hence the error. This was intentional at first as I wanted to test the 'events' module without the actual table module.
I was able to easily mock the 'angular-table' module by creating a new file in my test folder called 'mocks/angular-table.js' and added the following code:
/mocks/angular-table.js
'use-strict';
angular.module('angular-table', []);
I added this file to my karma.conf.js file, along with the real 'events' module I wanted to test:
karma.conf.js
...
files = [
JASMINE,
JASMINE_ADAPTER,
'scripts/libs/angular.js',
'scripts/libs/angular-mocks.js',
'scripts/events.js', // this is the real module.
'scripts/mocks/*.js', //loads all custom mocks.
'scripts/specs/*.spec.js' // loads my spec file.
]
...
Finally in my spec file, I was able to add both modules by calling them separately in a beforeEach block:
specs/events.spec.js
beforeEach(function(){
module('angular-table');
module('events');
});
I got the idea to structure my files in this way from this post
I recently released ngImprovedTesting that should make mock testing in AngularJS way easier.
In your case just use the following in your Jasmine test:
beforeEach(ModuleBuilder.forModule('events').serviceWithMocks('eventsCtrl').build());
For more information about ngImprovedTesting check out its introductory blog post: http://blog.jdriven.com/2014/07/ng-improved-testing-mock-testing-for-angularjs-made-easy/