Background: I'm using Jasmine2 as my test framework for Protractor and trying to implement a logger mechanism in the framework using winston package for better logging.
Problem: Test fails with below Non-Zero error in cmd which was working fine before including winston in the script. Could you please help me in implementing it correct.
Non-Zero Error:
Report destination: target\e2e\screenshots\my-report.html
[20:28:52] I/launcher - Running 1 instances of WebDriver
[20:28:52] I/hosted - Using the selenium server at http://127.0.0.1:4444/wd/hub
[20:28:56] I/launcher - 0 instance(s) of WebDriver still running
[20:28:56] I/launcher - chrome #01 failed 1 test(s)
[20:28:56] I/launcher - overall: 1 failed spec(s)
[20:28:56] E/launcher - Process exited with error code 1
Below are the corresponding files for reference:
Scenario_01.js:
describe('Scenario_01', function() {
var Logging = require('./scripts/LoggingMech.js');
var common = require('./scripts/CloseBrowsers.js');
var Login = require('./scripts/Login.js');
it('Login', function() {
browser.waitForAngularEnabled(false);
Login.login('admin','Adminpwd')
.catch(error => Logging.Logger.log(error));
});
afterAll(function(){
common.closeBrowsers();
});
});
Login.js:
var Login = function() {
this.login = async function(username, passwordKey){
await browser.get('http://testwebsite.com/showCust');
await element(by.name('USER')).sendKeys(username);
await element(by.name('PASSWORD')).sendKeys(passwordKey);
await element(by.xpath('/html/body/table/tbody/tr[2]/td/table/tbody/tr/td/table/tbody/tr[3]/td/form/input[9]')).click();
await element(by.name('YES')).click();
//browser.sleep(10000);
};
}
module.exports = new Login();
LoggingMech.js
const winston = require('winston');
var Logging = function() {
this.Logger = function(){
const logger = winston.createLogger({
level: 'info',
format: format.simple(),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'TodaysLog.log' })
]
});
};
}
module.exports = new Logging();
Problem
const logger = winston.createLogger...You are creating a winston reference but it's never passed anywhere. You are just storing it in a constant. You would have to make it a property of Logger attribute and then create a new instance of Logging.Logger and call log() on it.
Fix
You would have to make it a property of Logger attribute.
In LoggingMech.js you do this.logger = winston.createLogger instead of const logger = winston.createLogger
and then create a new instance of Logging.Logger and call log() on it.
var Logging = require('./scripts/LoggingMech.js');
// logs foo
(new Logging.Logger()).log('foo');
But I think you are doing it more complex as it is needed. For implementing the logger you could simply change your LoggingMech.js file to the following:
const winston = require('winston');
module.exports = winston.createLogger({
level: 'info',
format: format.simple(),
transports: [
new winston.transports.Console(),
new winston.transports.File({
filename: 'TodaysLog.log'
})
]
});
And call it like:
var Logging = require('./scripts/LoggingMech.js');
Logging.log('foo');
The configuration of the logger still is encapsulated within LoggingMech.js and it's way simpler to call. The code is also easier to maintain.
Update 1: Logging with winston
The winston.log() method accepts only a log object and not a string.
// logs "I am a log message."
winston.log({
level: 'info',
message: 'I am a log message.'
}
For logging plain strings you can use logger.info().
Update 2: Full winston logging example
A working log example. Following code works for me on my machinde:
winston.js
var winston = require('winston');
module.exports = winston.createLogger({
level: 'info',
transports: [
new winston.transports.Console(),
new winston.transports.File({
filename: 'TodaysLog.log'
})
]
});
index.js
var Logging = require('./winston');
var message = {
level: 'info',
message: 'Hello distributed log files!'
};
Logging.log(message);
Logging.info('sdf');
Logging.error(message);
Logging.warn(message);
logged content in TodaysLog.log
{"level":"info","message":"Hello distributed log files!"}
{"message":"sdf","level":"info"}
{"level":"error","message":"Hello distributed log files!"}
{"level":"warn","message":"Hello distributed log files!"}
#sylvanBregy: Below are the files with recommended changes for reference.
I could witness the test to be complete with successful login , but not sure why there was an error triggered in console.
Also, nothing was written in the logFile as well :(
Scenario_01
describe('Scenario_01', function() {
console.log("Into Scenario1");
var Logging = require('./scripts/LoggingMech.js');
var common = require('./scripts/CloseBrowsers.js');
var Login = require('./scripts/Login.js');
it('Login', function() {
browser.waitForAngularEnabled(false);
Login.login('admin','Adminpwd')
Logging.info("foo"); //Neither this worked nor the below
Logging.info();
});
afterAll(function(){
common.closeBrowsers();
});
});
Login.js
var Login = function() {
console.log("Into Login Function");
this.login = async function(username, passwordKey){
await browser.get('http://testwebsite.com/showCust');
await element(by.name('USER')).sendKeys(username);
await element(by.name('PASSWORD')).sendKeys(passwordKey);
await element(by.xpath('/html/body/table/tbody/tr[2]/td/table/tbody/tr/td/table/tbody/tr[3]/td/form/input[9]')).click();
await element(by.name('YES')).click();
//browser.sleep(10000);
};
}
module.exports = new Login();
LoggingMech.js
const winston = require('C:\\Program Files\\nodejs\\node_modules\\npm\\node_modules\\winston\\lib\\winston');
//const winston = require('winston');
module.exports = winston.createLogger({
level: 'info',
format: 'simple',
transports: [
new winston.transports.Console(),
new winston.transports.File({filename:'./logs/TodaysLog.log',level: 'info'})
],
});
Console Log
Report destination: target\e2e\screenshots\my-report.html
[20:22:58] I/launcher - Running 1 instances of WebDriver
[20:22:58] I/hosted - Using the selenium server at http://127.0.0.1:4444/wd/hub
Into Scenario1
Into Login Function
[20:23:03] I/launcher - 0 instance(s) of WebDriver still running
[20:23:03] I/launcher - chrome #01 failed 1 test(s)
[20:23:03] I/launcher - overall: 1 failed spec(s)
[20:23:03] E/launcher - Process exited with error code 1
Related
I am trying to figure out if there is a way to use a unique (per request) session ID in all of the winston logger calls when an HTTP request is made.
Elaboration on the issue:
Given a scenario that several hundred requests hit a website per minute and each request passes through different functions which log out various messages.
My goal is to log messages including a unique session ID per request using winston logger, until the response is sent.
I generate a unique session ID for the request using app.use(session(...)) from express-session library.
Using morgan, the HTTP logs are printed with a unique session ID like so:
logger = winston.createLogger(...);
const myStream = {
write: (text: string) => {
logger.info(text);
}
}
morgan.token('sessionid', function (req, res) { return req['sessionID'] });
app.use(morgan(':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" ["SESSION_ID :sessionid"]', { stream: myStream }));
However, I also want to use the same session ID in other logger.* functions elsewhere in the code. I am able to do that but as the number of simulataneous requests (using k6 load test) increases, the session ID gets overwritten by a new session ID of another request.
My code for using the session ID in request in a winston transport is:
public static initializeLogger(appInstance: express.Application) {
if (!appInstance) throw new Error(`Cannot initialize logger. Invalid express.Application instance passed. Logging may not be available`);
appInstance.use((req, res, next) => {
//this.m_sessionID = req["sessionID"];
this.m_logger.clear();
this.m_logger = winston.createLogger({
level: LOG_LEVEL,
levels: winston.config.syslog.levels,
format: winston.format.json(),
transports: [
new winston.transports.Console({ format: winston.format.simple() }),
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'debug.log', level: 'debug' }),
new WinstonCloudWatch({
logGroupName: CLOUDWATCH_LOG_GROUP_NAME,
logStreamName: function () {
let date = new Date().toISOString().split('T')[0];
return 'k-server-logs-' + date;
},
awsRegion: AWS_REGION,
awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID,
awsSecretKey: process.env.AWS_SECRET_ACCESS_KEY,
retentionInDays: process.env.CLOUDWATCH_LOG_RETENTION_DAYS ? Number(process.env.CLOUDWATCH_LOG_RETENTION_DAYS) : 30,
messageFormatter: (log) => {
return `${JSON.stringify({
message: log.message,
sessionID: req["sessionID"],
level: log.level
})}`
}
})
],
});
next();
});
}
I was hoping putting the winston logger in app.use(...) middleware would set up the cloudwatch transport for the winston logger along with using the req.sessionID as each request comes in.
However, this setup isn't working. If I send even 10 simultaneous requests, this code breaks and the sessionID is incorrectly stamped on logger.* messages and/or duplicated across multiple messages.
I reviewed other implementations such as https://solidgeargroup.com/en/express-logging-global-unique-request-identificator-nodejs/ but could not get it to work.
Hoping for some advice - I am sure my setup is off.
Thank you in advance.
Key hint from https://solidgeargroup.com/en/express-logging-global-unique-request-identificator-nodejs/
Use express-http-context which has a set and get function that will ensure that the unique session ID is available throughout your code.
import httpContext from 'express-http-context';
...
...
logger.add(new WinstonCloudWatch({
level:LOG_LEVEL,
logGroupName: CLOUDWATCH_LOG_GROUP_NAME,
logStreamName: function () {
let date = new Date().toISOString().split('T')[0];
return `${process.env.CLOUDWATCH_LOG_FILE_NAMEPREFIX}-logs-${date}`;
},
awsRegion: AWS_REGION,
awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID,
awsSecretKey: process.env.AWS_SECRET_ACCESS_KEY,
retentionInDays: process.env.CLOUDWATCH_LOG_RETENTION_DAYS ? Number(process.env.CLOUDWATCH_LOG_RETENTION_DAYS) : 30,
messageFormatter: (log) => {
return `${JSON.stringify({
message: log.message,
**sessionID: httpContext.get('reqId')**,
level: log.level
})}`
}
}));
Summary of Problem
Receiving : Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Timeout when trying to run a test with Jest and supertest.
Specs
Koa2 project, Jest/Supertest testing, Babel 7.9.0 recommended configuration
What I've tried
I have a simple test from the same file running which I omitted from the code below for brevity. I've also tried sending an HTTP request from the browser - this file is imported & 'listen'ed in a server file. The request is failing because it is blocked by a CORS policy - I think this is a problem for another day and isn't affecting my test timing out.
I also tried removed .callback() from the supertest(..) call:
const response = await supertest(app).post('/save-material');
at which point I get TypeError: app.dress is not a function.
Here is the content of my test file:
process.env.NODE_ENV = 'test';
const app = require('../../src/server/app.js')
const supertest = require('supertest')
test('save-material returns response', async() => {
const response = await supertest(app.callback()).post('/save-material');
expect(response.status).toBe(200);
expect(response.body.status).toBe('success');
expect(response.body.msg).toBe('Material saved')
});
Here is the content of the imported file (app.js) from above:
require('#babel/register'); // not entry point - but is entry point for some tests
const Koa = require('koa');
var Router = require('koa-router')
const app = new Koa();
const router = new Router();
router
.post('/save-material', async(ctx) => {
ctx.response = {
status: 'success',
msg: 'Material saved'
}
return ctx;
})
app.use(router.routes());
app.use(router.allowedMethods());
module.exports = app;
I am trying to use mock-sockets with Cypress, setting up the mock in the onBeforeLoad hook for cy.visit() in my beforeEach block. I can get one test to work but when the mock setup runs on the next beforeEach I get an error that A mock server is already listening on this url.
code under test:
(called from my React app's componentDidiMount)
subscribeToSettings(url: string): W3CWebSocket {
let settingsSubscription = new W3CWebSocket(url);
settingsSubscription.onopen = () => console.log('WebSocket Client Connected (settings)');
settingsSubscription.onclose = () => console.log('WebSocket Client Disconnected (settings)');
settingsSubscription.onmessage = (message: MessageEvent) => this.handleSettingsMessage(message);
return settingsSubscription;
}
/**
* Handler for websocket settings messages, which updates the local settings values.
* #param message the websocket message
*/
handleSettingsMessage(message: MessageEvent) {
const updatedValues = JSON.parse(message.data);
console.log('A message was received on the settings channel.', updatedValues);
this.props.updateSettingsFromBackend(updatedValues);
}
cypress tests
import { Server } from 'mock-socket'
import { defaultSettingsState } from "../../src/reducers/settings.reducer";
import { _createSettingsApiPutPayload } from "../../src/actions/settings.actions";
describe('mock socket method 1', () => {
let mockSocket;
let mockServer;
beforeEach(() => {
cy.visit('/', {
onBeforeLoad(win: Window): void {
// #ts-ignore
cy.stub(win, 'WebSocket', url => {
mockServer = new Server(url)
mockServer.on('connection', socket => {
console.log('mock socket connected');
mockSocket = socket;
});
mockSocket = new WebSocket(url);
return mockSocket
});
},
});
});
afterEach(() => {
mockSocket.close()
mockServer.stop()
});
it('gets a message', () => {
cy.contains('SETTINGS').click()
const object = _createSettingsApiPutPayload(defaultSettingsState)
mockSocket.send(JSON.stringify(object));
cy.contains('Motion threshold')
});
it('gets another message', () => {
cy.contains('SETTINGS').click()
const object = _createSettingsApiPutPayload(defaultSettingsState)
mockSocket.send(JSON.stringify(object));
cy.contains('Motion threshold')
});
});
Here are the logs from my console:
WebSocket Client Connected (settings)
mock socket connected at url ws://localhost:8702/PM_Settings
A message was received on the settings channel. {…}
mock socket connected at url ws://localhost:3000/sockjs-node/949/mhuyekl3/websocket
The development server has disconnected.
Refresh the page if necessary.
Uncaught Error: A mock server is already listening on this url
I wonder if it has to do with that second call which is for some mystery url.
(Note: calling cy.contains('SETTINGS').click() at the end of beforeEach somehow doesn't work, even in that first test. Even when I have my app set to start on the settings page (instead of having to click to it from inside the tests), clicking on SETTINGS from beforeEach still doesn't work even though we're already there. So that's kind of weird)
These cypress logs may also be helpful:
It only worked for me, when I moved server stopping into WebSocket stub:
cy.stub(window, 'WebSocket', url => {
if (mockServer) {
mockServer.stop();
}
mockServer = new Server(url);
mockServer.on('connection', socket => {
mockSocket = socket;
});
mockSocket = new WebSocket(url);
return mockSocket;
});
Im probably wrong, but I guess afterEach or mockServer.stop(); is async thats why mock server fails to stop before new init
I wish to render a page using Nuxt's renderAndGetWindow in order to test the values returned by my API.
Here's how I do it:
// Nuxt instance
let nuxt = null;
// Our page to test
let homePage = null;
beforeAll(async (done) => {
// Configuration
const rootDir = resolve(__dirname, '../..');
let config = {};
config = require(resolve(rootDir, 'nuxt.config.js'));
config.rootDir = rootDir; // project folder
config.env.isDev = false; // dev build
config.mode = 'universal'; // Isomorphic application
nuxt = new Nuxt(config);
await new Builder(nuxt).build();
nuxt.listen(3001, 'localhost');
homePage = await nuxt.renderAndGetWindow('http://localhost:3001/');
});
Which gives me 2 distinct errors:
console.error node_modules/jest-jasmine2/build/jasmine/Env.js:157
Unhandled error
console.error node_modules/jest-jasmine2/build/jasmine/Env.js:158
TypeError: setInterval(...).unref is not a function
And
Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.
at mapper (node_modules/jest-jasmine2/build/queue_runner.js:41:52)
I get this ever since I switched from Ava to Jest.
Am I handling my rendering wrong?
unref
The default test environment for Jest is a browser-like environment through jsdom.
unref is a special function provided by Node. It is not implemented in browsers or in jsdom, but it is implemented in the "node" test environment in Jest.
It looks like testing a Nuxt app requires both a Node environment to start a server, and a jsdom environment to test the resulting UI.
This can be done by setting the test environment to "node" and initializing a window using jsdom during the test setup.
timeout
Jest will "wait if you provide an argument to the test function, usually called done". This applies to test functions and setup functions like beforeAll.
Your beforeAll function has an argument done that is never called. Jest will wait until either done is called or the timeout configured with jest.setTimeout expires (defaults to 5 seconds).
You are using an async function and are using await on what looks to be the asynchronous part of the function so you don't need done. Change your beforeAll function to not take any parameters and that will prevent Jest from waiting for done to be called.
In my tests starting the Nuxt server takes quite a while so you can pass a timeout value as an additional parameter to beforeAll to increase the timeout for just that function.
Here is an updated test with these changes:
/**
* #jest-environment node
*/
// TODO: Set the environment to "node" in the Jest config and remove this docblock
// TODO: Move this to a setup file
const { JSDOM } = require('jsdom');
const { window } = new JSDOM(); // initialize window using jsdom
const resolve = require('path').resolve;
const { Nuxt, Builder } = require('nuxt');
// Nuxt instance
let nuxt = null;
// Our page to test
let homePage = null;
beforeAll(async () => {
// Configuration
const rootDir = resolve(__dirname, '../..');
let config = {};
config = require(resolve(rootDir, 'nuxt.config.js'));
config.rootDir = rootDir; // project folder
config.env.isDev = false; // dev build
config.mode = 'universal'; // Isomorphic application
nuxt = new Nuxt(config);
await new Builder(nuxt).build();
nuxt.listen(3001, 'localhost');
homePage = await nuxt.renderAndGetWindow('http://localhost:3001/');
}, 20000); // Give the beforeAll function a large timeout
afterAll(() => {
nuxt.close();
});
describe('homepage', () => {
it('should do something', () => {
});
});
I can get the test to run on the cloud, but it is failing. However, it works local. I think it is because I don't have the right server address. I tried myserver.bluemix.net, localhost:5001 and the null that works local. I can't seem to find the address.
my unit test:
process.env.NODE_ENV = 'test';
var chai = require('chai');
var chaiHttp = require('chai-http');
var app = require('../index');
var cfenv = require('cfenv');
var should = chai.should();
var expect = chai.expect;
chai.use(chaiHttp);
describe('Conversation', function() {
var serviceBaseUrl = '';
if (process.env.test_env == 'cloud') {
serviceBaseUrl
= 'http://' + '127.0.0.1:5001';
}
it ('should return message', function(done){
chai.request(app)
.post(serviceBaseUrl + '/api/v1/conversation')
.send({input: "test", ConverationId: ""})
.end(function (err, res) {
res.status.should.equal(200);
console.log(res.body);
done();
});
});
});
this is the error:
Server running at http://127.0.0.1:5001
Conversation
1) should return message
double callback!
0 passing (33ms)
1 failing
1) Conversation
should return message:
Uncaught TypeError: Cannot read property 'status' of undefined
at test/test-conversation.js:27:12
at Test.Request.callback (/home/pipeline/79a4adb4-e686-494a-9974-3c5860240fcb/node_modules/superagent/lib/node/index.js:615:12)
at ClientRequest.<anonymous> (/home/pipeline/79a4adb4-e686-494a-9974-3c5860240fcb/node_modules/superagent/lib/node/index.js:567:10)
at Socket.socketErrorListener (_http_client.js:309:9)
at emitErrorNT (net.js:1281:8)
at _combinedTickCallback (internal/process/next_tick.js:74:11)
at process._tickCallback (internal/process/next_tick.js:98:9)
If this is a unit test, you should use mocks in order to isolate your testing environment. The problem is that your localhost is going to have a different base url than when your app is deployed to the cloud.
Consider using a library like nock to mock your api requests.
If you are doing and/or want to do integration tests, you can set the base url with something like this:
const base = process.env['ROUTE'] || 'http://localhost:3000/route';
(Where process.env['ROUTE'] could be something like 'https://app.mybluemix.net/route'.)