TestCafe assertions over multiple selectors in Promise.allSettled seems misleading - testing

Consider the following tests:
import { Selector, t } from 'testcafe'
const validSelector = Selector('html')
const invalidSelector = Selector('non-existent')
//the following test passes and establishes a baseline for the assertions below
test
('baseline', async t => {
await t
.expect(validSelector.exists).ok()
.expect(invalidSelector.exists).notOk()
})
//here the first set of assertions pass, while the second fail
test
('Promise.allSettled is misleading', async t => {
let selectors = [validSelector, validSelector, invalidSelector]
let expected = ['fulfilled', 'fulfilled', 'rejected']
await Promise.allSettled(selectors.map(async selector => await t.expect(selector.exists).ok()))
.then(async o => await t.expect(JSON.stringify(o.map(result => result.status)) === JSON.stringify(expected)).ok())
selectors = [validSelector, invalidSelector, validSelector]
expected = ['fulfilled', 'rejected', 'fulfilled']
await Promise.allSettled(selectors.map(async selector => await t.expect(selector.exists).ok()))
.then(async o => await t.expect(JSON.stringify(o.map(result => result.status)) === JSON.stringify(expected)).ok(JSON.stringify(o.map(result => result.status))))
})
const handleExceptions = async (selector) =>
await t.expect(selector.exists).ok()
.then(f => true)
.catch(r => false)
//here all of the promises are fulfilled due to the exception handling,
//but as one may expect the results are the same as in the previous test -- the assertion at the end fails
test
('Promise.allSettled with exception handling is still misleading', async t => {
const selectors = [validSelector, invalidSelector, validSelector]
const expected = [true, false, true]
await Promise.allSettled(selectors.map(async selector => await handleExceptions(selector)))
.then(async o => await t.expect(JSON.stringify(o) === JSON.stringify(expected)).ok(JSON.stringify(o)))
})
//in this test the assertion actually passes
test
('Snapshots seem like the only viable option', async t => {
const tags = ['html', 'non-existent', 'html']
const expected = [true, false, true]
await Promise.allSettled(tags.map(async tag => await Selector(tag)))
.then(async selectors => await Promise.allSettled(selectors.map(async s => await s.value())))
.then(snapshots => snapshots.map(snapshot => snapshot.value != null))
.then(async o => await t.expect(JSON.stringify(o) === JSON.stringify(expected)).ok(JSON.stringify(o)))
})
The question is -- is the described behaviour of Promise.allSettled in the cases above expected due to some perk of TestCafe or the usage above is in any way wrong?
In case it's the former, then should users take the snapshots route as the last test suggests or there are other viable options (maybe safer, faster, etc.)?

It seems this issue is related to the following open TestCafe issue: https://github.com/DevExpress/testcafe/issues/5623. Please vote for it and subscribe to notifications to learn about our progress.

Related

Test NestJs API controller with Jest

I am creating an API with NestJs and mysql.
My controller function for create a new entity is working well, however, I can't test the usecase where the response is a 400 error.
This is the controller function :
#Controller('pubs')
export class PubsController {
constructor(private readonly pubsService: PubsService) {}
#Post()
async create(#Body() createPubDto: CreatePubDto, #Res() res: Response): Promise<void> {
this.pubsService.create(createPubDto)
.then(() => res.status(201).json())
.catch(err => res.status(401).json({ err }));
}
}
And this is the test file :
describe('PubsController', () => {
let controller: PubsController;
let service: PubsService;
const mockResponse = () => {
const res: any = {};
res.status = jest.fn().mockReturnValue(res);
res.json = jest.fn().mockReturnValue(res);
return res;
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [PubsController],
providers: [PubsService, {
provide: getRepositoryToken(Pub),
useValue: {},
}],
}).compile();
controller = module.get<PubsController>(PubsController);
service = module.get<PubsService>(PubsService);
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});
describe('create success', () => {
const res = mockResponse();
it('Should create a pub', async () => {
const req = mockedPub;
jest.spyOn(service, 'create').mockResolvedValue(mockedPub);
await controller.create(req, res);
expect(res.status).toHaveBeenCalledWith(201);
});
it('Should return 400 if the body is not correct', async () => {
const req: any = {};
jest.spyOn(service, 'create').mockResolvedValue(req);
await controller.create(req, res);
expect(res.status).toHaveBeenCalledWith(400);
});
})
});
"Should create a pub" is working well, but when I give to the create function an empty object, the test give me a 201 res.status.
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: 400
Received: 201
Does anybody know why?
First, do you use any validation pipe anywhere in your code in order to validate the incoming CreatePubDto, e.g. in your main.ts file or in the pubService ?
Second, in your test that should fail, you have written jest.spyOn(service, 'create').mockResolvedValue(req); which resolves, and thus you won't catch any error at the controller level, which means you go in the .then(() => res.status(201).json()) of your controller logic.
You should refactor the test to:
it('Should return 400 if the body is not correct', async () => {
const error: any = { message: 'bad DTO provided', code: 400 }; // <== this is where you mock the logic of your service to throw an error
jest.spyOn(service, 'create').mockRejectedValue(error);
await controller.create(req, res);
expect(res.status).toHaveBeenCalledWith(400);
});
This way you're telling Jest to throw an error when the create method is called. I put example error that could be thrown, but feel free to use your own error format that will be thrown.
Also don't forget to align your error code returned in your controller with the one expected in your test. 400 would be the more appropriate I guess in this use case.

How to write business logic in a service as sinon in ExpressJS

I want to use sinon to write test code for the business logic of a service using ExpressJS and Mongoose.
I wrote the following test code, but findOneService takes only id as an argument and returns a document with that id.
//service_test.js
const sinon = require('sinon');
// Service
const { findOneService } = require('../services/service');
// Schema
const Post = require('../models/mongoose/schemas/post');
describe('findOneService', () => {
let find;
beforeEach(() => {
find = sinon.stub(Post, 'findOne');
});
afterEach(() => {
find.restore();
});
it('should findOne', async () => {
const id = ???;
...?
});
})
//service.js
exports.findOneDocument = async (id) => {
const result = await Post.findOne({_id: id});
if (!result) {
throw new Error('404');
}
return result;
};
How can I define the result of this to pass the test code?
To test this kind of behaviour, I strongly suggest an integration test (with an embedded/dockerized MongoDB, for example). This would allow to test-drive more things than just the service, such as schema, migration, db config.
However, if you just want to test-drive the if (!result)... logic, you can stick with sinon. What you're missing is stubbing the return value:
it('returns the document if found', async () => {
find.returns('a post');
expect(await findOneService.findOneDocument('id')).toReturn('a post');
});
it('throw error when document does not exist', async () => {
find.returns(null);
expect(() => await findOneService.findOneDocument('non-existent id')).toThrow(Error);
});

Testcafe: How to update the store of userRole

Using userRole, I try to avoid having to log in before each test, but my test suite is running long, so the app goes fetching a new accessToken based on the current refershToken a number of times. This causes tests to fail sometimes, and after some investigation, I discovered that the localStorage seems only to be stored at login time, not at test exit time. So when a refresh happened, the next test does get the wrong accessToken and an expired refreshToken, so the app fails. (Which probably should not happen and simply present a login screen, but that is beside the point, as that is a scenario that should not happen in these tests.)
How can I make sure that the Role is updated with the latest version of localStorage upon exiting the test?
This is the test code to isolate the problem:
test('xxx', async t => {
await t.useRole(anAdminUser)
await ClientFunction(() => window.localStorage.setItem('xxx', 'xxx'))()
await t.expect(await ClientFunction(() => window.localStorage.getItem('xxx'))()).eql('xxx')
}
test('yyy', async t => {
await t.useRole(anAdminUser)
await t.expect(await ClientFunction(() => window.localStorage.getItem('xxx'))()).eql('xxx') /* => fail */
}
I finally worked around the problem using:
fixture('aaa')
.beforeEach(async t => {
await t.useRole(anAdminUser)
const { sessionCache } = anAdminUser
if (!anAdminUser.sessionCache) return
await ClientFunction(sessionCache => window.localStorage.setItem('xxx', sessionCache))(sessionCache)
})
.afterEach(async () => {
const sessionCache = await ClientFunction(() => window.localStorage.getItem('xxx'))()
anAdminUser.sessionCache = sessionCache
})
but it seems like a waste to duplicate some of the efforts of useRole itself.

How do I split my Jest + Puppeteer tests in multiple files?

I am writing automated tests using Jest & Puppeteer for a Front-end application written in Vue.js
So far I managed to write a set of tests, but they all reside in the same file:
import puppeteer from 'puppeteer';
import faker from 'faker';
let page;
let browser;
const width = 860;
const height = 1080;
const homepage = 'http://localhost:8001/brt/';
const timeout = 1000 * 16;
beforeAll(async () => {
browser = await puppeteer.launch({
headless: false, // set to false if you want to see tests running live
slowMo: 30, // ms amount Puppeteer operations are slowed down by
args: [`--window-size=${width},${height}`],
});
page = await browser.newPage();
await page.setViewport({ width, height });
});
afterAll(() => {
browser.close();
});
describe('Homepage buttons', () => {
test('Gallery Button', async () => {
// navigate to the login view
await page.goto(homepage);
await page.waitFor(1000 * 0.5); // without this, the test gets stuck :(
await page.waitForSelector('[data-testid="navBarLoginBtn"]');
await page.click('[data-testid="navBarLoginBtn"]'),
await page.waitForSelector('[data-testid="navBarGalleryBtn"]');
await page.click('[data-testid="navBarGalleryBtn"]'),
// test: check if we got to the gallery view (by checking nr of tutorials)
await page.waitForSelector('.card-header');
const srcResultNumber = await page.$$eval('.card-header', (headers) => headers.length);
expect(srcResultNumber).toBeGreaterThan(1);
}, timeout);
});
describe('Register', () => {
const btnLoginToRegister = '#btn-login-to-register';
const btnRegister = '#btn-register';
const btnToLogin = '#btn-goto-login';
test('Register failed attempt: empty fields', async () => {
// navigate to the register form page via the login button
await page.goto(homepage);
await page.waitForSelector(navLoginBtn);
await page.click(navLoginBtn);
await page.waitForSelector(btnLoginToRegister);
await page.click(btnLoginToRegister);
// test; checking for error messages
await page.waitForSelector(btnRegister);
await page.click(btnRegister);
const errNumber = await page.$$eval('#errMessage', (err) => err.length);
expect(errNumber).toEqual(3);
}, timeout);
test('Register failed: invalid char count, email format', async () => {
// fill inputs
await page.waitForSelector('#userInput');
await page.type('#userInput', 'a');
await page.waitForSelector('#emailInput');
await page.type('#emailInput', 'a');
await page.waitForSelector('#emailInput');
await page.type('#passInput', 'a');
await page.waitForSelector(btnRegister);
await page.click(btnRegister);
// test: check if we 3 errors (one for each row), from the front end validations
const err = await page.$$eval('#errMessage', (errors) => errors.length);
expect(err).toEqual(3);
}, timeout);
test('Register: success', async () => {
await page.click('#userInput', { clickCount: 3 });
await page.type('#userInput', name1);
await page.click('#emailInput', { clickCount: 3 });
await page.type('#emailInput', email1);
await page.click('#passInput', { clickCount: 3 });
await page.type('#passInput', password1);
await page.waitForSelector(btnRegister);
await page.click(btnRegister);
// test: check if go to login link appeared
await page.waitForSelector(btnToLogin);
await page.click(btnToLogin);
// await Promise.all([
// page.click(btnToLogin),
// page.waitForNavigation(),
// ]);
}, timeout);
test('Register failed: email already taken', async () => {
// navigate back to the register form
await page.waitForSelector(btnLoginToRegister);
await page.click(btnLoginToRegister);
await page.click('#userInput');
await page.type('#userInput', name2);
await page.click('#emailInput');
await page.type('#emailInput', email1); // <- existing email
await page.click('#passInput');
await page.type('#passInput', password2);
await page.click(btnRegister);
const err = await page.$eval('#errMessage', (e) => e.innerHTML);
expect(err).toEqual('Email already taken');
}, timeout);
});
I would like to be able to have a single test file that does the beforeAll and afterAll stuff, and each test suite: HomepageButtons, Register, etc. to reside in it's own test file. How would I be able to achieve this?
I've tried splitting tets into:
testsUtils.js that would contain the beforeAll and afterAll hooks and code but it doesn't guarantee that it runs when it needs: the beforeAll code to fire before all other test files and the afterAll code to fire after all the test files finished.
Sorry, I'd rather comment on your question, but I don't have reputation for that. Anyway, I think that you are looking for something like a "global beforeAll" and "global afterAll" hooks, right? Jest has it alread. It's called "globalSetup" and "globalTeardown".
Take a look at globalSetup. Excerpt:
This option allows the use of a custom global setup module which
exports an async function that is triggered once before all test
suites.
The Global Teardown one goes the same.
I think you'll have a headache trying to get a reference to the page or browser in globalSetup/globalTeardown and I confess that I never try this. Maybe the answer for that problem (if you have it) is on this page, under "Custom example without jest-puppeteer preset section.
Also there is a repo that tries to facilitate Jest + Puppeteer integration. Maybe you find it util: repo.
Good luck. :)

all mongoose queries stall

From previous answers this probably has something to do with my connection but I can't seem to place how to find the issue. I have a few segment of codes that looks like this and the result is a stall after any model function is called
connection - prints success when the server starts
mongoose.Promise = require('bluebird');
mongoose.connect('mongodb://localhost/test', { useMongoClient: true, promiseLibrary: require('bluebird') })
.then(() => console.log('connection succesful'))
.catch((err) => console.error(err));
model.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var holdingsSchema = new Schema({
pair: String,
amount: Number
});
var holdingsModel = mongoose.model('holdingsModel', holdingsSchema);
module.exports = holdingsModel
and api endpoint
holdingsModel.find(function(err, results) {
if (err) {
console.log('there is an error')
}
return res.json(results);
}).then(results => console.log('the results: ' + results));
});
At the end of everything I receive an err: empty response.
find() needs a criteria object. The second argument is the callback. When you are using promises you don't need the callback. Finally, you have to handle the errors with catch().
holdingsModel.find({ pair: 'test' }).exec()
.then(results => res.status(200).json(results))
.catch(err => res.status(400).json(err));
The exec() is used to return a proper promise, as mentioned here.