babel generated code breaking istanbul coverage - javascript

I'm using babel to enable ES6 imports in a node project. Also using mocha for testing, and istanbul for coverage. I end up with less than full coverage because babel generates code something like the following:
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _promise = require('babel-runtime/core-js/promise');
var _promise2 = _interopRequireDefault(_promise);
var _koa = require('koa');
var _koa2 = _interopRequireDefault(_koa);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
Specifically, the generated function _interopRequireDefault is copied into every code file, and the branches are not necessarily always executed, which skews the branch coverage number emitted for istanbul. Is there any way around this issue?

If you're using gulp, I have a gist with a gulpfile here that sets up the necessary hooks and filters. The relevant chunk is to load isparta, hook require, and let the tests run:
gulp.task('test:cover', (cb) => {
gulp.src('src/main/**/*.js')
.pipe(istanbul({
instrumenter: require('isparta').Instrumenter,
includeUntested: true
}))
.pipe(babel())
.pipe(gulp.dest('target/cover'))
.pipe(istanbul.hookRequire())
.on('finish', cb);
});
gulp.task('test:mocha', (cb) => {
gulp.src('target/test/**/Test*')
.pipe(mocha())
.pipe(istanbul.writeReports())
.on('end', cb);
});
gulp.task('test', (cb) => {
return runSequence('test:cover', 'test:mocha', cb);
});
The only frustrating part is that your tests must use the covered code:
import {
LinearInterpolator,
CosineInterpolator
} from '../../cover/random/Interpolators';
I haven't found a way to work around that yet without also covering the test scripts and skewing coverage, although you should be able to do that by merging streams:
gulp.task('test:cover', (cb) => {
const src = gulp.src('src/main/**/*.js')
.pipe(istanbul({
instrumenter: require('isparta').Instrumenter,
includeUntested: true
}));
const test = gulp.src('src/test/**/*.js');
merge(src, test)
.pipe(babel())
.pipe(gulp.dest('target/cover'))
.pipe(istanbul.hookRequire())
.on('finish', cb);
});

You need to combine it with isparta - https://github.com/douglasduteil/isparta - to get the coverage working correctly. I warn you its a bit trial and error at the moment! My npm script looks like -
"coverage": "node_modules/.bin/babel-node node_modules/.bin/isparta cover --include-all-sources --report html node_modules/.bin/_mocha -- --reporter $npm_package_config_bdd_reporter",

We've run into this and I finally got fed up and looked into what causes this line. It turns out that every time to use an import like:
import chai from 'chai';
this babel fill gets added to allow sane interaction with older export styles. The trouble is that none of the common libraries exhibit the "true" branch of the ternary. I build the following file coverInterrop.js that artificially trips the first branch using old-school exports:
module.exports = {
__esModule: true
};
and I include in any file where I want to use an undestructured import:
// eslint-disable-next-line no-unused-vars
import coverInterrop from 'coverInterrop';
Note that it has to assign to a variable to trip the coverage and good eslint rules won't like that

Related

Scoping issue with terser-webpack-plugin options

I have a situation where I don't have Terser configured correctly and it's causing the compressed version of my page to break. My issue is that I have some functional declarations in my index.js. These declarations need to be accessible from Bootstrap modal windows that can be loaded in at a later time.
For example, in my index.js there is a function declared like this:
function doThis() { }
Then, say, a user opens an address form in a modal window, and a different javascript file called 'address-form.js' is loaded. In this form there is a button with an onclick handler that calls doThis(). The onclick handler lives in 'address-form.js' but is able to access doThis() in the parent index.js. The button works fine when index.js isn't compressed. But after it's compressed, I get an error saying doThis() doesn't exist.
I believe this to be a scoping issue, because I do see the doThis() declaration in index.js, but it appears to be wrapped in a bunch of parentheses. I'm not sure how to make the scope of doThis() the top-level window. The same scoping issue exists for literally hundreds of function declarations and variables in index.js, so I'm looking for a solution where I don't have to tinker too much with the gargantuan file.
Notice if I change the function declaration to an expression, it DOES seem to work:
window.doThis = function() {
}
However, because there are hundreds of vars and const and let variables in the file (in addition to dozens of function declarations), it's not really practical for me to change the scope of all of them just so the compression will work.
Here is my webpack.config.js:
const TerserPlugin = require("terser-webpack-plugin")
const glob = require('glob')
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: glob.sync('./js/Pages/*.js').reduce((object, element) => {
object[path.parse(element).name] = element
return object
}, {}),
output: {
filename: '[name].js',
path: path.resolve(__dirname, './js/Pages/minified')
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true,
test: /\.js(\?.*)?$/i,
terserOptions: {
mangle: false,
compress: true,
keep_fnames: true,
keep_classnames: true,
ie8: false,
safari10: false,
toplevel: false
}
})
]
},
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1
})
]
}
The command I'm running is:
webpack --mode=production --config webpack.config.js
SOLUTION
Solution as accepted below was to use terser-cli directly. I wrote a little script that runs terser on every file in a directory if anyone should find it helpful:
const fs = require('fs')
const path = require('path')
const exec = require('child_process').exec;
const Terser = require('terser')
const srcDir = '../js/Pages'
const destDir = '../js/Pages/minified'
function minifyPagesDir() {
let srcFileNames = fs.readdirSync(srcDir).filter(path => path.match(/\.js$/)) || []
let srcFilePaths = []
let destFilePaths = srcFileNames.map((item, i) => {
srcFilePaths[i] = `${srcDir}/${srcFileNames[i]}`
return `${destDir}/${item}`
})
if (!fs.existsSync(destDir))
fs.mkdirSync(destDir)
srcFileNames.forEach((item, i) => {
exec(
`terser ${srcFilePaths[i]} -c -o ${destFilePaths[i]} --ecma 2021`,
(error, stdout, stderr) => {
if (error !== null)
console.log(`RUHROH: ${error}`, ' stderr: ', stderr);
}
)
console.log(`Minified '${srcFilePaths[i]}' to '${destFilePaths[i]}'!`)
})
console.log('Minification complete!')
}
minifyPagesDir()
I created a repro project with your configuration and verified that your problems are not caused by terser, but by webpack.
I've used the following test file:
function doThis() { }
With minimize disabled and no minimizer set, this is the result of building the test file with webpack:
/******/ (() => { // webpackBootstrap
var __webpack_exports__ = {};
function doThis() { }
/******/ })()
;
If we reformat it and remove the comments, it's clear that everything inside the file is dead code. Webpack is a bundler and wraps everything in an IIFE. This means that functions won't be assigned to global scope.
(() => {
var __webpack_exports__ = {};
function doThis() { }
})();
Since you're not exporting anything or causing any side-effects, like assigning to window, terser will remove the content. This is correct and even without removing it, you still would get the same errors, since the function in webpack's output is not assigned to the global scope and your handlers couldn't access it. Compressing webpack's export with the terser cli results in an empty file.
Running the terser cli directly on the test input file, results in the desired compressed output:
function doThis(){}
You said you've ran terser from the command line and had the same problems as when bundling and compressing through webpack. Did you run the source files through terser or the output of webpack? Because running the source files through terser should result in the desired output. Please try running terser foo/bar.js -c -o foo/bar.min.js and test if it solves your problem.
If you want to use webpack, you need to either assign to window or move away from using global scope altogether.
I don't know when it was added, but webpack has an IIFE option.
https://webpack.js.org/configuration/output/#outputiife
But unfortunately, it always generates unnecessary code var __webpack_exports__ = {}.
I don't know how to delete that code.
I would like to know if there is an option.

Running a test with Mocha also launches the main program

I'm trying to use Mocha to test a CLI app. The tests are running fine but, when I launch the testing procedure, it also launches the main app:
$ npm run test
> standardize-js#0.2.2 test C:\Users\Gaspard\Documents\Code\standardize-js
> mocha "./source/**/*.spec.js"
? Choose your project language or framework (Use arrow keys) //<-- THIS IS THE PROGRAM
> Javascript
Typescript
AngularJS
Main function //<-- THIS IS THE TEST
ask if the configuration is valid
Configuration is not valid, terminating program.
√ should return false if the configuration is not accepted
1 passing (29ms)
I'm kind of new to the testing world and I'm really struggling to understand what I'm doing wrong.
Here is the NPM script used to launch mocha :
"test": "mocha \"./source/**/*.spec.js\""
Here is my testing method:
/* eslint-disable func-names */
const { expect } = require("chai");
const main = require("./index").test;
describe("Main function", function() {
describe("ask if the configuration is valid", function() {
it("should return false if the configuration is not accepted", function() {
const fakeAnswer = false;
expect(main.validateConfiguration(fakeAnswer)).to.equal(false);
});
});
});
And here is my index.js file:
function validateConfiguration(answer) {
if (answer === false) {
console.log(chalk.red("Configuration is not valid, terminating program."));
return false;
}
return true;
}
const run = async () => {
//MAIN FUNCTION
};
run();
// Export functions and variables to be able to test
exports.test = {
validateConfiguration
};
It's not a problem with mocha. It is simply now node.js modules work.
When you do this:
const main = require("./index").test;
Node.js will execute index.js and then check the value of module.exports. If the module (index.js) sets or modifies module.exports then node will export it for use by require(). But note, in order for node to know that the module has exported anything it must execute the javascript file.
Node.js does not have any ability to parse and analyze javascript syntax (that's V8's job). Unlike other languages such as C or Java, modules in node.js are not implemented at the syntax level. Therefore the javascript language does not need to be modified (eg. ES6 modules) for node.js to support modules. Modules are simply implemented as a design pattern.
In your index.js file you call run:
run();
When require() loads index.js it will therefore also cause run() to be called.
Test libraries, not main
The solution to this is to implement your own logic as modules and test that, not test index.js:
mylib.js:
function validateConfiguration(answer) {
if (answer === false) {
console.log(chalk.red("Configuration is not valid, terminating program."));
return false;
}
return true;
}
// Export functions and variables to be able to test
exports.test = { validateConfiguration };
index.js:
const validateConfiguration = require("./mylib").test;
const run = async () => {
//MAIN FUNCTION
};
run();
You can now use your test script as written.
How can you not test code??
The strategy to keep index.js bug free without testing is to remove all logic from it except for the minimum amount of code to wire all your other code up together to run the app. The code should be as simple as "Hello World". That way, the code in main is so small and so simple that you can test it for bugs using your eyeballs.
Any code in index.js that causes a bug should be refactored into its own library so that it can be tested separately. There are a small handful of corner cases, such as loading environment variables or opening port 80 where you can't really separate into a library because they literally are wiring logic. For such cases you just have to be really careful.
It's calling run because you are telling it to right after defining the method.

require.context not a function when trying to run Mocha tests

I'm trying to run tests using the Mocha.js + JSDOM frameworks, but I'm having trouble getting Mocha to start up. This is in the process of testing a React app using the Vue.js library. I keep getting the following error:
var req = require.context('./', false, /\.vue$/);
TypeError: require.context is not a function
The code in question is:
let req = require.context('./', false, /\.vue$/);
components.forEach(function (component) {
try {
let filePath = './' + component + '.vue';
let injected = inject(req(filePath));
Vue.component(getComponentName(component), injected);
let appComponent = {
name: injected.name,
props: {
autocompletion: {
metadata: getComponentName('template'),
score: xTemplatesScore,
attributes: injected.props || []
}
}
};
appComponents.push(appComponent);
} catch (err) {
console.log(err);
console.error('Vue file was not found for component:' + component + '. Please rename your files accordingly ( component-name.vue )');
}
Is there a way to get around this and actually get Mocha to start up? Or is there a suitable replacement for require.context? I've tried to redo it with just plain string concatenations and a vanilla require, but that keeps telling me that none of the Vue modules can be found.
require.context is a method of webpack. Your tests must be bundled before they can be run.
Normally, you'd create a separate webpack config file for your tests. You'll then create a test bundle using webpack and then run Mocha on this bundle. Alternatively, you can use mocha-loader inside the webpack test config file and let the tests run as part of the bundling process.
Further information can be found in the webpack documentation on testing.

How can I mock Webpack's require.context in Jest?

Suppose I have the following module:
var modulesReq = require.context('.', false, /\.js$/);
modulesReq.keys().forEach(function(module) {
modulesReq(module);
});
Jest complains because it doesn't know about require.context:
FAIL /foo/bar.spec.js (0s)
● Runtime Error
- TypeError: require.context is not a function
How can I mock it? I tried using setupTestFrameworkScriptFile Jest configuration but the tests can't see any changes that I've made in require.
I had the same problem, then I've made a 'solution'.
I'm pretty sure that this is not the best choice. I ended up stopping using it, by the points answered here:
https://github.com/facebookincubator/create-react-app/issues/517
https://github.com/facebook/jest/issues/2298
But if you really need it, you should include the polyfill below in every file that you call it (not on the tests file itself, because the require will be no global overridden in a Node environment).
// This condition actually should detect if it's an Node environment
if (typeof require.context === 'undefined') {
const fs = require('fs');
const path = require('path');
require.context = (base = '.', scanSubDirectories = false, regularExpression = /\.js$/) => {
const files = {};
function readDirectory(directory) {
fs.readdirSync(directory).forEach((file) => {
const fullPath = path.resolve(directory, file);
if (fs.statSync(fullPath).isDirectory()) {
if (scanSubDirectories) readDirectory(fullPath);
return;
}
if (!regularExpression.test(fullPath)) return;
files[fullPath] = true;
});
}
readDirectory(path.resolve(__dirname, base));
function Module(file) {
return require(file);
}
Module.keys = () => Object.keys(files);
return Module;
};
}
With this function, you don't need to change any require.context call, it will execute with the same behavior as it would (if it's on webpack it will just use the original implementation, and if it's inside Jest execution, with the polyfill function).
After spending some hours trying each of the answers above. I would like to contribute.
Adding babel-plugin-transform-require-context plugin to .babelrc for test env fixed all the issues.
Install - babel-plugin-transform-require-context here https://www.npmjs.com/package/babel-plugin-transform-require-context (available with yarn too)
Now add plugin to .babelrc
{
"env": {
"test": {
"plugins": ["transform-require-context"]
}
}
}
It will simply transform require-context for test env into dummy fn calls so that code can run safely.
If you are using Babel, look at babel-plugin-require-context-hook. Configuration instructions for Storybook are available at Storyshots | Configure Jest to work with Webpack's require.context(), but they are not Storyshots/Storybook specific.
To summarise:
Install the plugin.
yarn add babel-plugin-require-context-hook --dev
Create a file .jest/register-context.js with the following contents:
import registerRequireContextHook from 'babel-plugin-require-context-hook/register';
registerRequireContextHook();
Configure Jest (the file depends on where you are storing your Jest configuration, e.g. package.json):
setupFiles: ['<rootDir>/.jest/register-context.js']
Add the plugin to .babelrc
{
"presets": ["..."],
"plugins": ["..."],
"env": {
"test": {
"plugins": ["require-context-hook"]
}
}
}
Alternatively, add it to babel.config.js:
module.exports = function(api) {
api.cache(true)
const presets = [...]
const plugins = [...]
if (process.env.NODE_ENV === "test") {
plugins.push("require-context-hook")
}
return {
presets,
plugins
}
}
It may be worth noting that using babel.config.js rather than .babelrc may cause issues. For example, I found that when I defined the require-context-hook plugin in babel.config.js:
Jest 22 didn't pick it up;
Jest 23 picked it up; but
jest --coverage didn't pick it up (perhaps Istanbul isn't up to speed with Babel 7?).
In all cases, a .babelrc configuration was fine.
Remarks on Edmundo Rodrigues's answer
This babel-plugin-require-context-hook plugin uses code that is similar to Edmundo Rodrigues's answer here. Props to Edmundo! Because the plugin is implemented as a Babel plugin, it avoids static analysis issues. e.g. With Edmundo's solution, Webpack warns:
Critical dependency: require function is used in a way in which dependencies cannot be statically extracted
Despite the warnings, Edmundo's solution is the most robust because it doesn't depend on Babel.
Extract the call to a separate module:
// src/js/lib/bundle-loader.js
/* istanbul ignore next */
module.exports = require.context('bundle-loader?lazy!../components/', false, /.*\.vue$/)
Use the new module in the module where you extracted it from:
// src/js/lib/loader.js
const loadModule = require('lib/bundle-loader')
Create a mock for the newly created bundle-loader module:
// test/unit/specs/__mocks__/lib/bundle-loader.js
export default () => () => 'foobar'
Use the mock in your test:
// test/unit/specs/lib/loader.spec.js
jest.mock('lib/bundle-loader')
import Loader from 'lib/loader'
describe('lib/loader', () => {
describe('Loader', () => {
it('should load', () => {
const loader = new Loader('[data-module]')
expect(loader).toBeInstanceOf(Loader)
})
})
})
Alrighty! I had major issues with this and managed to come to a solution that worked for me by using a combination of other answers and the Docs. (Took me a good day though)
For anyone else who is struggling:
Create a file called bundle-loader.js and add something like:
module.exports = {
importFiles: () => {
const r = require.context(<your_path_to_your_files>)
<your_processing>
return <your_processed_files>
}
}
In your code import like:
import bundleLoader from '<your_relative_Path>/bundle-loader'
Use like
let <your_var_name> = bundleLoader.importFiles()
In your test file right underneath other imports:
jest.mock('../../utils/bundle-loader', () => ({
importFiles: () => {
return <this_will_be_what_you_recieve_in_the_test_from_import_files>
}
}))
Installing
babel-plugin-transform-require-context
package and adding the plugin in the .babelrc resolved the issue for me.
Refer to the documentation here:
https://www.npmjs.com/package/babel-plugin-transform-require-context
The easiest and fastest way to solve this problem will be to install require-context.macro
npm install --save-dev require-context.macro
then just replace:
var modulesReq = require.context('.', false, /\.js$/);
with:
var modulesReq = requireContext('.', false, /\.js$/);
Thats it, you should be good to go!
Cheers and good luck!
Implementation problems not mentioned:
Jest prevents out-of-scope variables in mock, like __dirname.
Create React App limits Babel and Jest customization. You need to use src/setupTests.js which is run before every test.
fs is not supported in the browser. You will need something like browserFS. Now your app has file system support, just for dev.
Potential race condition. Export after this import. One of your require.context imports includes that export. I'm sure require takes care of this, but now we are adding a lot of fs work on top of it.
Type checking.
Either #4 or #5 created undefined errors. Type out the imports, no more errors. No more concerns about what can or can't be imported and where.
Motivation for all this? Extensibility. Keeping future modifications limited to one new file. Publishing separate modules is a better approach.
If there's an easier way to import, node would do it. Also this smacks of premature optimization. You end up scrapping everything anyways because you're now using an industry leading platform or utility.
If you're using Jest with test-utils in Vue.
Install these packages:
#vue/cli-plugin-babel
and
babel-plugin-transform-require-context
Then define babel.config.js at the root of the project with this configuration:
module.exports = function(api) {
api.cache(true);
const presets = [
'#vue/cli-plugin-babel/preset'
];
const plugins = [];
if (process.env.NODE_ENV === 'test') {
plugins.push('transform-require-context');
}
return {
presets,
plugins
};
};
This will check if the current process is initiated by Jest and if so, it mocks all the require.context calls.
I faced the same issue with an ejected create-react-app project
and no one from the answers above helped me...
My solution were to copy to config/babelTransform.js the follwoing:
module.exports = babelJest.createTransformer({
presets: [
[
require.resolve('babel-preset-react-app'),
{
runtime: hasJsxRuntime ? 'automatic' : 'classic',
},
],
],
plugins:["transform-require-context"],
babelrc: false,
configFile: false,
});
Simpleset Solution for this
Just Do
var modulesReq = require.context && require.context('.', false, /\.js$/);
if(modulesReq) {
modulesReq.keys().forEach(function(module) {
modulesReq(module);
});
}
So Here I have added extra check if require.context is defined then only execute By Doing this jest will no longer complain

How to properly require modules from mocha.opts file

I'm using the expect.js library with my mocha unit tests. Currently, I'm requiring the library on the first line of each file, like this:
var expect = require('expect.js');
describe('something', function () {
it('should pass', function () {
expect(true).to.be(true); // works
});
});
If possible, I'd like to remove the boilerplate require code from the first line of each file, and have my unit tests magically know about expect. I thought I might be able to do this using the mocha.opts file:
--require ./node_modules/expect.js/index.js
But now I get the following error when running my test:
ReferenceError: expect is not defined
This seems to make sense - how can it know that the reference to expect in my tests refers to what is exported by the expect.js library?
The expect library is definitely getting loaded, as if I change the path to something non-existent then mocha says:
"Error: Cannot find module './does-not-exist.js'"
Is there any way to accomplish what I want? I'm running my tests from a gulp task if perhaps that could help.
You are requiring the module properly but as you figured out, the symbols that the module export won't automatically find themselves into the global space. You can remedy this with your own helper module.
Create test/helper.js:
var expect = require("expect.js")
global.expect = expect;
and set your test/mocha.opts to:
--require test/helper
While Louis's answer is spot on, in the end I solved this with a different approach by using karma and the karma-chai plugin:
Install:
npm install karma-chai --save-dev
Configure:
karma.set({
frameworks: ['mocha', 'chai']
// ...
});
Use:
describe('something', function () {
it('should pass', function () {
expect(true).to.be(true); // works
});
});
Thanks to Louis answer and a bit of fiddling around I sorted out my test environment references using mocha.opts. Here is the complete setup.
My project is a legacy JavaScript application with a lot of "plain" js files which I wish to reference both in an html file using script tags and using require for unit testing with mocha.
I am not certain that this is good practice but I am used to Mocha for unit testing in node project and was eager to use the same tool with minimal adaptation.
I found that exporting is easy:
class Foo{...}
class Bar{...}
if (typeof module !== 'undefined') module.exports = { Foo, Bar };
or
class Buzz{...}
if (typeof module !== 'undefined') module.exports = Buzz;
However, trying to use require in all the files was an issue as the browser would complain about variables being already declared even when enclosed in an if block such as:
if (typeof require !== 'undefined') {
var {Foo,Bar} = require('./foobar.js');
}
So I got rid of the require part in the files and set up a mocha.opts file in my test folder with this content. The paths are relative to the root folder:
--require test/mocha.opts.js
mocha.opts.js content. The paths are relative to the location of the file:
global.assert = require('assert');
global.Foo = require("../foobar.js").Foo;
global.Bar = require("../foobar.js").Bar;
global.Buzz = require("../buzz.js");

Categories