How to use CodeceptJS to unit-test a JS function - unit-testing

I've set up CodeceptJS for a project and use it to test various end-to-end scenarios.
Now I want to extend the tests-suite to also run unit-tests to verify functionality of custom JS functions.
For example: I have a global object App that has a version attribute. As a first test, I want to confirm that App.version is present and has a value.
My first attempt is a test.js file with the following code:
Feature('Unit Tests');
Scenario('Test App presence', ({ I }) => {
I.amOnPage('/');
I.executeScript(function() {return App.version})
.then(function(value) { I.say(value) } );
});
Problems with this code
The major issue: How can I assert that the App.version is present?
My script can display the value but does not fail if it's missing
My code is very complex for such a simple test.
I'm sure there's a cleaner/faster way to perform that test, right?

Here is a solution that works for me:
Read data from the browser:
I created a custom helper via npx codecept gh and named it BrowserAccess.
The helper function getBrowserData uses this.helpers['Puppeteer'].page.evaluate() to run and return custom code from the browser scope. Documentation for .evaluate()
Custom assertions:
Install the codeceptjs-assert package, e.g. npm i codeceptjs-assert
Add the AssertWrapper-helper to the codecept-config file. This enables checks like I.assert(a, b)
Full Code
codecept.conf.js
exports.config = {
helpers: {
AssertWrapper: {
require: "codeceptjs-assert"
},
BrowserAccess: {
require: './browseraccess_helper.js'
},
...
},
...
}
browseraccess_helper.js
const Helper = require('#codeceptjs/helper');
class BrowserAccess extends Helper {
async getBrowserData(symbolName) {
const currentPage = this.helpers['Puppeteer'].page;
let res;
try {
res = await currentPage.evaluate((evalVar) => {
let res;
try {
res = eval(evalVar);
} catch (e) {
}
return Promise.resolve(res);
}, symbolName);
} catch (err) {
res = null;
}
return res;
}
}
jsapp_test.js (the test is now async)
Feature('Unit Tests');
Scenario('Test App presence', async ({ I }) => {
I.amOnPage('/');
const version = await I.getBrowserData('App.version');
I.assertOk(version);
});

Related

How to set/mock an env variable in vitest (`process.env.NODE_ENV = 'anything'` takes effect only in test file)?

I have a method, in a class, that only executes its action when NODE_ENV === 'test'.
Here is the test that I set the env to anything to test the failing scenario:
it('returns Left on clearDatabase when not in test environment', async () => {
const { sut } = await makeSut()
process.env.NODE_ENV = 'any_environment'
const result = await sut.clearDatabase()
process.env.NODE_ENV = 'test'
expect(result.isLeft()).toBe(true)
})
Here is the method:
async clearDatabase (): Promise<Either<Error, void>> {
if (process.env.NODE_ENV !== 'test') {
return left(new Error('Clear database is allowed only in test environment'))
}
try {
const { database } = this.props.dataSource
await this.mongoClient.db(database).dropDatabase()
return right()
} catch (error) {
return left(error)
}
}
The problem is that when the method do it's verification, the value in NODE_ENV wasn't changed at all, it has its initial value (test). If I log the value, after setting it, in test file it's there, only the object can't see this change. In jest it works just fine. How can I set/mock it properly in vitest?
Here you find a StackBlitz with an example scenario: https://stackblitz.com/edit/node-lr72gz?file=test/example.unit.test.ts&view=editor

How to write unit test with Higher Order Function with Jest in React JS

Hello i'm new to React and i'm trying to write a unit test on a Higher Order Functions with Jest and i don't know how to do it please can someone help me ? This is the code of my HIGHER ORDER FUNCTION below :
const updateSearchTopStoriesState = (hits, page) => (prevState) => {
const { searchKey, results } = prevState
const oldHits = results && results[searchKey]
? results[searchKey].hits
: []
const updatedHits = [
...oldHits,
...hits
]
// returning our previous state
return {
results: {
...results,
[searchKey]: { hits: updatedHits, page }
},
isLoading: false
}
}
export default updateSearchTopStoriesState
Without knowing WHAT you are trying to test, or WHAT the shape of any of the parameters are, this is near impossible to answer accurately. Here are a couple of unit tests I would write:
describe("updateSearchTopStoriesState", () => {
it("should return a function", () => {
expect(typeof updateSearchTopStoriesState()).toBe("function");
});
it("should return default return value", () => {
const { results, isLoading } = updateSearchTopStoriesState()({
searchKey: "test"
});
expect(results).toEqual({ test: { hits: [] } });
expect(isLoading).toBe(false);
});
});
In the sandbox I've started a third test that currently fails (admittedly likely due to my lack of context on the parameters, but should be passing based upon an internal implementation comment you left in the function code).
This should assist you in starting a unit test file for this function, but please comment if anything is unclear or this isn't quite what you are asking about.

How to mock DialogService.open(...).whenClosed(...) with Jasmine?

We have some TypeScript code using the Aurelia framework and Dialog plugin that we are trying to test with Jasmine, but can't work out how to do properly.
This is the source function:
openDialog(action: string) {
this._dialogService.open({ viewModel: AddAccountWizard })
.whenClosed(result => {
if (!result.wasCancelled && result.output) {
const step = this.steps.find((i) => i.action === action);
if (step) {
step.isCompleted = true;
}
}
});
}
We can create a DialogService spy, and verify the open method easily - but we can't work out how to make the spy invoke the whenClosed method with a mocked result parameter so that we can then assert that the step is completed.
This is the current Jasmine code:
it("opens a dialog when clicking on incomplete bank account", async done => {
// arrange
arrangeMemberVerificationStatus();
await component.create(bootstrap);
const vm = component.viewModel as GettingStartedCustomElement;
dialogService.open.and.callFake(() => {
return { whenClosed: () => Promise.resolve({})};
});
// act
$(".link, .-arrow")[0].click();
// assert
expect(dialogService.open).toHaveBeenCalledWith({ viewModel: AddAccountWizard });
expect(vm.steps[2].isCompleted).toBeTruthy(); // FAILS
done();
});
We've just recently updated our DialogService and ran into the same issue, so we've made this primitive mock that suited our purposes so far. It's fairly limited and doesn't do well for mocking multiple calls with different results, but should work for your above case:
export class DialogServiceMock {
shouldCancelDialog = false;
leaveDialogOpen = false;
desiredOutput = {};
open = () => {
let result = { wasCancelled: this.shouldCancelDialog, output: this.desiredOutput };
let closedPromise = this.leaveDialogOpen ? new Promise((r) => { }) : Promise.resolve(result);
let resultPromise = Promise.resolve({ closeResult: closedPromise });
resultPromise.whenClosed = (callback) => {
return this.leaveDialogOpen ? new Promise((r) => { }) : Promise.resolve(typeof callback == "function" ? callback(result) : null);
};
return resultPromise;
};
}
This mock can be configured to test various responses, when a user cancels the dialog, and scenarios where the dialog is still open.
We haven't done e2e testing yet, so I don't know of a good way to make sure you wait until the .click() call finishes so you don't have a race condition between your expect()s and the whenClosed() logic, but I think you should be able to use the mock in the test like so:
it("opens a dialog when clicking on incomplete bank account", async done => {
// arrange
arrangeMemberVerificationStatus();
await component.create(bootstrap);
const vm = component.viewModel as GettingStartedCustomElement;
let mockDialogService = new MockDialogService();
vm.dialogService = mockDialogService; //Or however you're injecting the mock into the constructor; I don't see the code where you're doing that right now.
spyOn(mockDialogService, 'open').and.callThrough();
// act
$(".link, .-arrow")[0].click();
browser.sleep(100)//I'm guessing there's a better way to verify that it's finished with e2e testing, but the point is to make sure it finishes before we assert.
// assert
expect(mockDialogService.open).toHaveBeenCalledWith({ viewModel: AddAccountWizard });
expect(vm.steps[2].isCompleted).toBeTruthy(); // FAILS
done();
});

How to mock a Node.js module loaded with dojo/node

I have an application with the server code running on Node.js and using Dojo. I have a config module defined like:
define([
'dojo/node!nconf',
'dojo/_base/config'
], function (nconf, dojoConfig) {
nconf.argv().file({
file: dojoConfig.baseDir + '/config.json'
});
console.log('-- file name:', dojoConfig.baseDir + '/config.json');
console.log('-- context:', nconf.get('context'));
// ... logic here ...
return nconf.get(nconf.get('context'));
});
To be able to unit test this module, I've written two mocks: one for the nconf native module and one for dojoConfig. Here is the test:
define([
'require',
'intern!object',
'intern/chai!assert'
], function (require, registerSuite, assert) {
registerSuite({
name: 'config utility',
'load default settings': function () {
require.undef('dojo/node!nconf');
require.undef('dojo/_base/config');
require({ map: {
'*': {
'dojo/node!nconf': 'server/utils/tests/nconfMock',
'dojo/_base/config': 'server/utils/tests/dojoConfigMock'
}
}});
require(['../config', './nconfMock'], this.async(1000).callback(
function (config, nconfMock) {
assert.isNotNull(config);
assert.isNotNull(nconf);
// assert.deepEqual(config, nconfMock.contextSettings.test);
}
));
}
});
});
I can see that my mock of dojoConfig is correctly loaded, but not the mock of the nconf module. During a webcast on Intern, Dylan mentioned that the mapping does not consider the plugin, that there's the way to force dojo/node module to load this nconfMock. Would you mind to give me more details?
Obviously, this is verbose, so if this continues to be a common request, we’ll probably do something to make it simpler in the future.
Important note: Without mapping dojo/node to intern/node_modules/dojo/node, the loading of my initial config module as defined above fails in the Intern environment. The mapping is done in the intern.js file. The reported error is:
Error: node plugin failed to load because environment is not Node.js
at d:/git/fco2/src/libs/dojo/node.js:3:9
at execModule (d:\git\fco2\node_modules\intern\node_modules\dojo\dojo.js:512:54)
at d:\git\fco2\node_modules\intern\node_modules\dojo\dojo.js:579:7
at guardCheckComplete (d:\git\fco2\node_modules\intern\node_modules\dojo\dojo.js:563:4)
at checkComplete (d:\git\fco2\node_modules\intern\node_modules\dojo\dojo.js:571:27)
at onLoadCallback (d:\git\fco2\node_modules\intern\node_modules\dojo\dojo.js:653:7)
at d:\git\fco2\node_modules\intern\node_modules\dojo\dojo.js:758:5
at fs.js:266:14
at Object.oncomplete (fs.js:107:15)
Solution: As suggested by Colin Snover below, I now use Mockery. I also do NOT use the contextual require, only the default one. Here is a (simplified) solution working with the version 1.9.3 of the Dojo toolkit.
define([
'intern!object',
'intern/chai!assert',
'intern/node_modules/dojo/node!mockery',
'./nconfMock'
], function (registerSuite, assert, mockery, nconfMock) {
registerSuite({
name: 'config utility',
teardown: function () {
mockery.disable();
mockery.deregisterAll();
require({ map: { '*': { 'dojo/_base/config': 'dojo/_base/config' } } });
require.undef('dojo/_base/config');
require.undef('server/utils/config');
},
'load default settings': function () {
mockery.enable();
mockery.registerMock('nconf', nconfMock);
require({ map: { '*': { 'dojo/_base/config': 'server/utils/tests/dojoConfigMock' } } });
require.undef('dojo/_base/config');
require.undef('server/utils/config');
require(
['server/utils/config'],
this.async(1000).callback(function (config) {
assert.isNotNull(config);
assert.deepEqual(config, nconfMock.contextSettings.test);
})
);
}
});
});
Thanks, Dom
In order to mock a Node.js dependency, you will probably want to simply use one of the various available projects for mocking Node.js modules. Mockery is a good choice since it’s stand-alone.
Since it looks like you’re using dojo/node and not the one from Intern, in your case, you’d do it like this:
define([
'intern!object', 'dojo/node!mockery', 'dojo/Deferred', 'require'
], function (registerSuite, mockery, Deferred, require) {
var moduleUsingMock;
registerSuite({
setup: function () {
var dfd = new Deferred();
mockery.enable();
mockery.registerMock('module-to-mock', mockObject);
require([ 'module-using-mock' ], function (value) {
moduleUsingMock = value;
dfd.resolve();
});
return dfd.promise;
},
teardown: function () {
mockery.disable();
},
'some test': function () {
moduleUsingMock.whatever();
// ...
}
});
});

How do I mock an Angular service using jasmine?

This may be a duplicate but I have looked at a lot of other questions here and they usually miss what I am looking for in some way. They mostly talk about a service they created themselves. That I can do and have done. I am trying to override what angular is injecting with my mock. I thought it would be the same but for some reason when I step through the code it is always the angular $cookieStore and not my mock.
I have very limited experience with jasmine and angularjs. I come from a C# background. I usually write unit tests moq (mocking framework for C#). I am use to seeing something like this
[TestClass]
public PageControllerTests
{
private Mock<ICookieStore> mockCookieStore;
private PageController controller;
[TestInitialize]
public void SetUp()
{
mockCookieStore = new Mock<ICookieStore>();
controller = new PageController(mockCookieStore.Object);
}
[TestMethod]
public void GetsCarsFromCookieStore()
{
// Arrange
mockCookieStore.Setup(cs => cs.Get("cars"))
.Return(0);
// Act
controller.SomeMethod();
// Assert
mockCookieStore.VerifyAll();
}
}
I want mock the $cookieStore service which I use in one of my controllers.
app.controller('PageController', ['$scope', '$cookieStore', function($scope, $cookieStore) {
$scope.cars = $cookieStore.get('cars');
if($scope.cars == 0) {
// Do other logic here
.
}
$scope.foo = function() {
.
.
}
}]);
I want to make sure that the $cookieStore.get method is invoked with a 'garage' argument. I also want to be able to control what it gives back. I want it to give back 0 and then my controller must do some other logic.
Here is my test.
describe('Controller: PageController', function () {
var controller,
scope,
cookieStoreSpy;
beforeEach(function () {
cookieStoreSpy = jasmine.createSpyObj('CookieStore', ['get']);
cookieStoreSpy.get.andReturn(function(key) {
switch (key) {
case 'cars':
return 0;
case 'bikes':
return 1;
case 'garage':
return { cars: 0, bikes: 1 };
}
});
module(function($provide) {
$provide.value('$cookieStore', cookieStoreSpy);
});
module('App');
});
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
scope = $rootScope.$new();
controller = $controller;
}));
it('Gets car from cookie', function () {
controller('PageController', { $scope: scope });
expect(cookieStoreSpy.get).toHaveBeenCalledWith('cars');
});
});
This is a solution for the discussion we had in my previous answer.
In my controller I'm using $location.path and $location.search. So to overwrite the $location with my mock I did:
locationMock = jasmine.createSpyObj('location', ['path', 'search']);
locationMock.location = "";
locationMock.path.andCallFake(function(path) {
console.log("### Using location set");
if (typeof path != "undefined") {
console.log("### Setting location: " + path);
this.location = path;
}
return this.location;
});
locationMock.search.andCallFake(function(query) {
console.log("### Using location search mock");
if (typeof query != "undefined") {
console.log("### Setting search location: " + JSON.stringify(query));
this.location = JSON.stringify(query);
}
return this.location;
});
module(function($provide) {
$provide.value('$location', locationMock);
});
I didn't have to inject anything in the $controller. It just worked. Look at the logs:
LOG: '### Using location set'
LOG: '### Setting location: /test'
LOG: '### Using location search mock'
LOG: '### Setting search location: {"limit":"50","q":"ani","tags":[1,2],"category_id":5}'
If you want to check the arguments, spy on the method
// declare the cookieStoreMock globally
var cookieStoreMock;
beforeEach(function() {
cookieStoreMock = {};
cookieStoreMock.get = jasmine.createSpy("cookieStore.get() spy").andCallFake(function(key) {
switch (key) {
case 'cars':
return 0;
case 'bikes':
return 1;
case 'garage':
return {
cars: 0,
bikes: 1
};
}
});
module(function($provide) {
$provide.value('cookieStore', cookieStoreMock);
});
});
And then to test the argument do
expect(searchServiceMock.search).toHaveBeenCalledWith('cars');
Here is an example https://github.com/lucassus/angular-seed/blob/81d820d06e1d00d3bae34b456c0655baa79e51f2/test/unit/controllers/products/index_ctrl_spec.coffee#L3 it's coffeescript code with mocha + sinon.js but the idea is the same.
Basically with the following code snippet you could load a module and substitute its services:
beforeEach(module("myModule", function($provide) {
var stub = xxx; //... create a stub here
$provide.value("myService", stub);
}));
Later in the spec you could inject this stubbed service and do assertions:
it("does something magical", inject(function(myService) {
subject.foo();
expect(myService).toHaveBeenCalledWith("bar");
}));
More details and tips about mocking and testing you could find in this excellent blog post: http://www.yearofmoo.com/2013/09/advanced-testing-and-debugging-in-angularjs.html
Why mock cookieStore when you may use it directly without modification? The code below is a partial unit test for a controller which uses $cookieStore to put and get cookies. If your controller has a method known as "setACookie" that uses $cookieStore.put('cookieName', cookieValue) ... then the test should be able to read the value that was set.
describe('My controller', function() {
var $cookieStore;
describe('MySpecificController', function() {
beforeEach(inject(function(_$httpBackend_, $controller, _$cookieStore_) {
$cookieStore = _$cookieStore_;
// [...] unrelated to cookieStore
}));
it('should be able to reference cookies now', function () {
scope.setACookie();
expect($cookieStore.get('myCookieName')).toBe('setToSomething');
});
});