How to avoid looping expect when asserting on stdout from child - node.js

I'm trying to write an integration test in Jest with TypeScript for a logging utility that has some logic and wraps Gulp Fancy Log. I can't use Jest's spyOn with console (no stdout event occurs on the spy), I think because fancy log resets console.log.
My approach is to spawn a fixture to test the logging utility with ts-node, and then subscribe to the data event of the child process stdout stream. But it's called twice, first with fancy log's work and second with the param provided to log:
test('prints a string given to the logging function', done => {
const loggerInstance = path.resolve(process.cwd(), 'logger_string_param.ts')
const sut = spawn('ts-node', [loggerInstance])
sut.stdout.on('data', data => {
// can't expect here, it's called twice, first with `[16:41:50]`
// or similar and then with `something happened`
console.log(data.toString())
})
sut.on('exit', () => done())
})
And the fixture:
import { log } from '../utils'
log(`something happened`)
I would like to assert inside the data event listener, in the order the assertion is called (e.g. different assertions for first and second call). How can I do that (or refactor my test to achieve it)?

Related

Testing a function that is subscribing to an event

I'm trying to test a function that is subscribing to receive events notifications when a file is changed.
the function is the following:
const ccm2Listener = () => {
fs.watch(FILE_PATH, (eventType, fileName) => {
console.log(eventType);
console.log(fileName);
// modify the env based on the configurations in the file that changed
});
};
export default ccm2Listener;
The test:
import ccm2Listener from './ccm2';
it('should update the environment when the ccm2 config file is updated',() => {
const CCM2_TEST_VALUE = 'Hello World';
const CCM2_TEST_KEY = 'CCM2_TEST';
// invoking the function we want to test
ccm2Listener();
// aux function that modifies the file in order to trigger the event
addEnvToFile(FILE_PATH, { key: CCM2_TEST_KEY, value: CCM2_TEST_VALUE });
expect(process.env.CCM2_TEST).toBe(CCM2_TEST_VALUE);
});
The problem seems to be that Jest is finishing it's execution before the callback is trigger because I'm getting the following:
Cannot log after tests are done. Did you forget to wait for something async in your test?
Attempted to log "rename".
and
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
Anybody with a clue about how to handle this kind of testing?
Thanks!

How to mock a 'request' node module for testing in jest

I am new to writing test cases in jest and i wanted to test 'mark' function and want to mock 'request' node module. let's say this file's name is app.js and test file will be app.test.js
Can someone tell how to write its test case?
const request = require("request")
var test = {
mark: function(data,cb){
data.url = "localhost"
request(data, function(err,response,body){
if(!response){
err.response = false
}
cb(err,body)
})
}
}
module.exports = test;
If I understand your question correctly, you are asking two questions:
How to write the test and mock the request
What test cases you should write
Regarding the mock, I recommend using nock which is an npm module for mocking network requests.
To the second question, I believe that you should map the logic in the function and create the test cases from there. Think about the function, look at every if else/calculation/loop and it’s possible outcomes and create test cases from there.
The mark function doesn’t have a lot of logic, it’s send a request, updates the err variable according to the response and calling the callback.
The only outcome we can test to to see that if the request works, the cb is called correctly without modifications. And if the request returns empty, change the err var and call the callback.
To do so we need to mock the request and return a response to match the test case and validate that the cb was called correctly (can be done with a mock).
Test case example:
The test case may be a bit abstract since I don’t have the real use case of the function but it shows the point
it("Should mark response false when it does not exist", () => {
const DATA = {} // your data object
const callback = jest.fn();
nock('localhost')
.get('/example')
.reply(500) // Exaple of mocking the resonse. Custom it to be more specific depending on mark.
test.mark(DATA, callback);
// Verify that the function was called.
expect(callback).toHaveBeenCalled();
// Verify that `err.response` was false.
expect(callback.mock.calls[0][0].response).toBe(false)
})
You can read more about mocking and verifying parameters here.

Verifying a text on a dialog box with Cypress and Cucumber

I'm trying to verify a text message on a dialog box using cypress and cucumber. The test cases are working perfectly fine when it's within "it function". Here is is the sample code :
it ('Verify if the Login is successful', function()
{
cy.visit('loginTest.html')
cy.get('#username').type('shahin')
cy.get('#password').type('tala')
cy.contains('Login').click()
cy.on('window:alert', (str) => {
expect(str).to.equal(`Login Successfully`)
})
})
However when i add the BDD keywords it looks like the function doesn't get evaluated at all. It works for When but not the Then scenario. I think it needs to be handled in Js in a different way. I have uploaded the cypress log as well. Below is the code :
When('I click on the login button', () => {
cy.contains('Login').click()
})
Then('Successful POP up message should be displayed', () => {
cy.on('window:alert', (str) => {
expect(str).to.equal(`Login Successfully`)
})
Cypress Log
The first thing is cy.on('window:alert'... is a passive event listener, it does not do anything until an event is emitted by the app.
This means you need to set it before the event is triggered (e.g Login click),
When('I click on the login button', () => {
cy.on('window:alert', ...something here...) // set up the event listener
cy.contains('Login').click() // action that triggers the event
})
If you do your expect() within the callback of the event listener it mucks up your BDD flow (Then() is redundant).
Use a stub to catch the event, and assert the stub properties inside Then().
let stub // declare outside so it's visible in both When and Then
When('I click on the login button', () => {
stub = cy.stub() // set stub here (must be inside a test)
cy.on('window:alert', stub) // capture call
cy.contains('Login').click()
})
Then('message is displayed', () => {
expect(stub).to.have.been.calledWith('Login Successful')
})
Why does it() work?
Essentially, with it() all the code is within one block vs two blocks for When() Then().
The async commands are queued for later exectution, but the synchronous cy.on() is executed immediately - even though it's the last line it gets executed first.
it('...', () => {
// Queued and executed (slightly) later
cy.visit('loginTest.html')
cy.get('#username').type('shahin')
cy.get('#password').type('tala')
cy.contains('Login').click()
// executed immediately (so actually first line to run)
cy.on('window:alert', (str) => {
expect(str).to.equal(`Login Successfully`)
})
})
The When() and Then() blocks are executed in sequence, so you don't get the same pattern as with it().

how to send email (spawn mail) from gjs gtk app

I am trying to write a gjs app that needs to send emails.
The way I have found to do this is using spawn_async_with_pipes() to call mail.
The app seems to spawn mail, and I don't get an error, but I don't get any useful output nor do I get the test emails...
I have been at this for a while now and have found little to no useful up to date documentation. I am working with gtk3 and gjs (and glib). I have also tried spawning a shell script that in turn calls mail. This resulted in "could not resolve host" errors and a dead letter queue. So I know that I am spawning my command. I am not concerned about the "could not resolve host command", but by the fact that I can't get it by spawning mail directly.
I am spawning mail like this:
const [res, pid, in_fd, out_fd, err_fd] =
await GLib.spawn_async_with_pipes(null,
['mail',
'-V',
`-s "${msgObj.subBlock}"`,
`-r ${to}`,
`-S smtp=${HOST}`,
'-S smtp-use-starttls',
'-S smtp-auth=login',
`-S smtp-auth-user=${USER}`,
`-S smtp-auth-password=${PASS}`,
FROM
], null, GLib.SpawnFlags.SEARCH_PATH, null);
const in_reader = new Gio.DataOutputStream({
base_stream: new Gio.UnixOutputStream({fd: in_fd})
});
var feedRes = in_reader.put_string(msgObj.msgBlock, null);
const out_reader = new Gio.DataInputStream({
base_stream: new Gio.UnixInputStream({fd: out_fd})
});
const err_reader = new Gio.DataInputStream({
base_stream: new Gio.UnixInputStream({fd: err_fd})
});
var out = out_reader.read_until("", null);
var err = err_reader.read_until("", null);
print(` > out : "${out}"`);
print(` > res : "${res}"`);
print(` > feedRes : "${feedRes}"`);
print(` > err : "${err}"`);
err is 0, and res is just true
I don't know what the output should be, but I'm not getting a recognizable error, and no email is being delivered...
How can I get my app to send emails? Is spawning mail not the way to go?
Thanks in advance for any pointers you can give me.
There's couple things here I think are confusing you I think I can clear up.
await GLib.spawn_async_with_pipes(
GLib has it's own concept of async functions, that when applicable need to be wrapped in a Promise to work effectively with the await keyword. In this case, GLib.spawn_async_with_pipes() is not asynchronous in the way you're thinking, but that's okay because we're going to use the higher level class Gio.Subprocess.
async function mail(msgObj, to, host, user, pass, cancellable = null) {
try {
let proc = new Gio.Subprocess({
argv: ['mail',
'-V',
// Option switches and values are separate args
'-s', `"${msgObj.subBlock}"`,
'-r', `${to}`,
'-S', `smtp=${host}`,
'-S', 'smtp-use-starttls',
'-S', 'smtp-auth=login',
'-S', `smtp-auth-user=${user}`,
'-S', `smtp-auth-password=${pass}`,
FROM
],
flags: Gio.SubprocessFlags.STDIN_PIPE |
Gio.SubprocessFlags.STDOUT_PIPE |
Gio.SubprocessFlags.STDERR_MERGE
});
// Classes that implement GInitable must be initialized before use, but
// you could use Gio.Subprocess.new(argv, flags) which will call this for you
proc.init(cancellable);
// We're going to wrap a GLib async function in a Promise so we can
// use it like a native JavaScript async function.
//
// You could alternatively return this Promise instead of awaiting it
// here, but that's up to you.
let stdout = await new Promise((resolve, reject) => {
// communicate_utf8() returns a string, communicate() returns a
// a GLib.Bytes and there are "headless" functions available as well
proc.communicate_utf8_async(
// This is your stdin, which can just be a JS string
msgObj.msgBlock,
// we've been passing this around from the function args; you can
// create a Gio.Cancellable and call `cancellable.cancel()` to
// stop the command or any other operation you've passed it to at
// any time, which will throw an "Operation Cancelled" error.
cancellable,
// This is the GAsyncReady callback, which works like any other
// callback, but we need to ensure we catch errors so we can
// propagate them with `reject()` to make the Promise work
// properly
(proc, res) => {
try {
let [ok, stdout, stderr] = proc.communicate_utf8_finish(res);
// Because we used the STDERR_MERGE flag stderr will be
// included in stdout. Obviously you could also call
// `resolve([stdout, stderr])` if you wanted to keep both
// and separate them.
//
// This won't affect whether the proc actually return non-
// zero causing the Promise to reject()
resolve(stdout);
} catch (e) {
reject(e);
}
}
);
});
return stdout;
} catch (e) {
// This could be any number of errors, but probably it will be a GError
// in which case it will have `code` property carrying a GIOErrorEnum
// you could use to programmatically respond to, if desired.
logError(e);
}
}
Gio.Subprocess is a better choice overall, but especially for language bindings that can't pass "out" arguments into functions. Using GLib.spawn_async_with_pipes you would usually pass in NULL to prevent opening any pipes you didn't want, and always ensure you close any pipes you don't. Since we can't do that in GJS, you can end up with dangling file descriptors you can't close.
Gio.Subprocess does a lot of leg work for you and ensures file descriptors are closing, prevents zombie processes, sets up child watches for you and other things you really don't want to worry about. It also has convenience functions for getting IO streams so you don't have to wrap the fd's yourself, among other useful things.
I wrote a longer primer on async programming in GJS that you might find helpful here. You should be able to breeze though it pretty quickly, and it tries to clear up some confusion about the relationship between GLib async, JavaScript async and the GLib main loop vs JS event loop.

How to mock test a Node.js CLI with Jest?

I'm stuck at the very beginning, simply requiring the CLI and capturing its output. I've tried two methods but both don't work.
This is my cli.js:
#!/usr/bin/env node
console.log('Testing...');
process.exit(0);
And this my cli.test.js:
test('Attempt 1', () => {
let stdout = require("test-console").stdout;
let output = stdout.inspectSync(function() {
require('./cli.js');
});
expect(output).toBe('Testing...');
});
test('Attempt 2', () => {
console.log = jest.fn();
require('./cli.js');
expect(console.log.calls).toBe(['Testing...']);
});
Doesn't really matter which test is actually being run, the output is always:
$ jest
RUNS bin/cli.test.js
Done in 3.10s.
Node.js CLI applications are no different to other applications except their reliance on environment. They are expected to extensively use process members, e.g.:
process.stdin
process.stdout
process.argv
process.exit
If any of these things are used, they should be mocked and tested accordingly.
Since console.log is called directly for output, there's no problem to spy on it directly, although helper packages like test-console can be used too.
In this case process.exit(0) is called in imported file, so spec file early exits, and next Done output is from parent process. It should be stubbed. Throwing the error is necessary so that code execution is stopped - to mimic the normal behavior:
test('Attempt 2', () => {
const spy = jest.spyOn(console, 'log');
jest.spyOn(process, 'exit').mockImplementationOnce(() => {
throw new Error('process.exit() was called.')
});
expect(() => {
require('./cli.js');
}).toThrow('process.exit() was called.');
expect(spy.mock.calls).toEqual([['Testing...']]);
expect(process.exit).toHaveBeenCalledWith(0);
});

Resources