How to mock dependencies when testing GAS with CLASP - unit-testing

Background
I recently learned about CLASP and became excited about the possibility of using TDD to edit my Google Apps Scripts (GAS) locally.
NOTE: there might be a way to write tests using the existing GAS editor, but I'd prefer to use a modern editor if at all possible
clasp works great, but I cannot figure out how to mock dependencies for unit tests (primarily via jest, though I'm happy to use any tool that works)
I got farthest by using the gas-local package, and was able to mock a single dependency within a test
However I could not find a way to mock multiple dependencies in a single test/call, and so I created this issue
Challenge
Despite installing #types/google-apps-script, I am unclear on how to "require" or "import" Google Apps Script modules whether using ES5 or ES2015 syntax, respectively--see below for an illustration of this.
Related StackOverflow Post
Although there is a similar SO question on unit testing here, most of the content/comments appear to be from the pre-clasp era, and I was unable to arrive at a solution while following up the remaining leads. (Granted, it's very possible my untrained eye missed something!).
Attempts
Using gas-local
As I mentioned above, I created an issue (see link above) after trying to mock multiple dependencies while using gas-local. My configuration was similar to the jest.mock test I describe below, though it's worth noting the following differences:
I used ES5 syntax for the gas-local tests
My package configuration was probably slightly different
Using jest.mock
LedgerScripts.test.js
import { getSummaryHTML } from "./LedgerScripts.js";
import { SpreadsheetApp } from '../node_modules/#types/google-apps-script/google-apps-script.spreadsheet';
test('test a thing', () => {
jest.mock('SpreadSheetApp', () => {
return jest.fn().mockImplementation(() => { // Works and lets you check for constructor calls
return { getActiveSpreadsheet: () => {} };
});
});
SpreadsheetApp.mockResolvedValue('TestSpreadSheetName');
const result = getSummaryHTML;
expect(result).toBeInstanceOf(String);
});
LedgerScripts.js
//Generates the summary of transactions for embedding in email
function getSummaryHTML(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var dashboard = ss.getSheetByName("Dashboard");
// Do other stuff
return "<p>some HTML would go here</p>"
}
export default getSummaryHTML;
Result (after running jest command)
Cannot find module '../node_modules/#types/google-apps-script/google-apps-script.spreadsheet' from 'src/LedgerScripts.test.js'
1 | import { getSummaryHTML } from "./LedgerScripts.js";
> 2 | import { SpreadsheetApp } from '../node_modules/#types/google-apps-script/google-apps-script.spreadsheet';
| ^
3 |
4 | test('test a thing', () => {
5 | jest.mock('SpreadSheetApp', () => {
at Resolver.resolveModule (node_modules/jest-resolve/build/index.js:307:11)
at Object.<anonymous> (src/LedgerScripts.test.js:2:1)
For reference, if I go to the google-apps-script.spreadsheet.d.ts file that has the types I want, I see the following declarations at the top of the file...
declare namespace GoogleAppsScript {
namespace Spreadsheet {
...and this one at the bottom of the file:
declare var SpreadsheetApp: GoogleAppsScript.Spreadsheet.SpreadsheetApp;
So maybe I am just importing SpreadsheetApp incorrectly?
Other files
jest.config.js
module.exports = {
clearMocks: true,
moduleFileExtensions: [
"js",
"json",
"jsx",
"ts",
"tsx",
"node"
],
testEnvironment: "node",
};
babel.config.js
module.exports = {
presets: ["#babel/preset-env"],
};
package.json
{
"name": "ledger-scripts",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"author": "",
"license": "ISC",
"dependencies": {
"#babel/core": "^7.11.1",
"#babel/preset-env": "^7.11.0",
"#types/google-apps-script": "^1.0.14",
"#types/node": "^14.0.27",
"babel-jest": "^26.3.0",
"commonjs": "0.0.1",
"eslint": "^7.6.0",
"eslint-plugin-jest": "^23.20.0",
"gas-local": "^1.3.1",
"requirejs": "^2.3.6"
},
"devDependencies": {
"#types/jasmine": "^3.5.12",
"#types/jest": "^26.0.9",
"jest": "^26.3.0"
}
}

Note: the scope of your question is broad and may require clarification.
clasp works great, but I cannot figure out how to mock dependencies for unit tests (primarily via jest, though I'm happy to use any tool that works)
You don't need Jest or any particular testing framework to mock the global Apps Script objects.
// LedgerScripts.test.js
import getSummaryHTML from "./LedgerScripts.js";
global.SpreadsheetApp = {
getActiveSpreadsheet: () => ({
getSheetByName: () => ({}),
}),
};
console.log(typeof getSummaryHTML() === "string");
$ node LedgerScripts.test.js
true
So maybe I am just importing SpreadsheetApp incorrectly?
Yes, it is incorrect to import .d.ts into Jest.
Jest doesn't need the TypeScript file for SpreadsheetApp. You can omit it.
You only need to slightly modify the above example for Jest.
// LedgerScripts.test.js - Jest version
import getSummaryHTML from "./LedgerScripts";
global.SpreadsheetApp = {
getActiveSpreadsheet: () => ({
getSheetByName: () => ({}),
}),
};
test("summary returns a string", () => {
expect(typeof getSummaryHTML()).toBe("string");
});
Despite installing #types/google-apps-script, I am unclear on how to "require" or "import" Google Apps Script modules whether using ES5 or ES2015 syntax
#types/google-apps-script does not contain modules and you do not import them. These are TypeScript declaration files. Your editor, if it supports TypeScript, will read those files in the background and suddenly you'll have the ability to get autocomplete, even in plain JavaScript files.
Additional comments
Here you check that a function returns a string, perhaps just to make your example very simple. However, it must be stressed that such testing is better left to TypeScript.
Since you returned an HTML string, I feel obligated to point out the excellent HTML Service and templating abilities of Apps Script.
Unit testing or integration testing? You mention unit testing, but relying upon globals is generally a sign you might not be unit testing. Consider refactoring your functions so they receive objects as input rather than calling them from the global scope.
Module syntax: if you use export default foo, you then import without curly braces: import foo from "foo.js" but if you use export function foo() { then you use the curly braces: import { foo } from "foo.js"

Related

Jest: Cannot read property of undefined when importing from own package nextjs

Got this weird bug when running the jest test, one of the UI component from a self defined UI package keeps throwing error, saying that an object in that package is undefined...
The component itself works perfectly fine, and the same component's testing logic works in another repo without nextjs, and that repo utilize #swc/jest for js transform in jest.config file.
I've also added that package itself to transformIgnorePatterns in jest-config file, but somehow the bug still presents...
The project itself is in nextjs, and below is a snapshot of the jest.config file
/** #type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
testPathIgnorePatterns: [
'<rootDir>/.next/',
'<rootDir>/node_modules/',
'<rootDir>/e2e/'
],
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['#testing-library/jest-dom/extend-expect'],
setupFiles: [require.resolve('whatwg-fetch')],
transform: {
'^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }]
},
transformIgnorePatterns: ['/node_modules/myPackage', 'jest-runner'],
testMatch: ['**/*.spec.{js,jsx,ts,tsx}'],
};
and the error itself goes Error: Uncaught [TypeError: Cannot read property 'object' of undefined], which tracks down to /node_modules/myPackage
how the package is used
import { InputBox } from 'myPackage';
const MyComponent = () => {
return (
<div>
<InputBox />
</div>
);
}
export default MyComponent;
and here's the test:
import { act, render } from '#testing-library/react';
import React from 'react';
describe('show component', () => {
it('should render', async () => {
await act(async () => {
render(
<MyComponent/>
);
});
});
});
I've found a similar question on stackoverflow Jest: Cannot read property of undefined when importing from own package
but that one is using regular js, and this one being next.js, there's really nowhere I can update .babelrc to update those configs...
Any input would be appreciated.
Update: it turns out the component that causes me bug is built on top of react-popper library, which is built on top of popper.js library. popper.js library doesn't support jsdom by default, and it requires to do jest mock. But my library is 2 layers abstractions on top of popper.js library, I'm not sure how to do that, or even if that is doable...

How to write unit test in nestjs and typeorm without connecting to db

I am not sure how to implement unit test in nestjs and typeorm without connecting to db. I have tried a number of technic but non seem to work.
My module looks something like this.
import { HttpModule, Module } from '#nestjs/common'
import moment from 'moment';
import config from '#app/config'
import { OrdersService } from './services/order.service'
import { FraudOrderChecksService } from './services/fraud-order-checks.service'
import { FraudOrderChecksController } from './controllers/fraud-order-checks.controller'
import { HealthcheckController } from './controllers/healthcheck.controller';
import { TypeOrmModule } from '#nestjs/typeorm'
import { ormconfig } from './entities/ormconfig'
#Module({
imports: [
SharedModule,
HttpModule,
LoggerModule,
ConfigModule.forRoot(config),
TypeOrmModule.forRoot(ormconfig.luminskin as any),
TypeOrmModule.forRoot(ormconfig.meridian as any),
TypeOrmModule.forFeature([...ormconfig.luminskin.entities], 'luminskin'),
TypeOrmModule.forFeature([...ormconfig.meridian.entities], 'meridian'),
...
],
controllers: [
MyController,
...
],
providers: [
...
],
})
export class AppModule { }
I import the root module in my test
beforeEach(async () => {
jest.clearAllMocks();
const module: TestingModule = await Test.createTestingModule({
imports: [InternalModule]
}).compile();
...
});
When I try to run my unit test I get
[Nest] 93196 - 06/04/2021, 17:43:50 [ExceptionHandler] Unable to connect to the database (mydb). Retrying (1)...
AlreadyHasActiveConnectionError: Cannot create a new connection named "connectionname", because connection with such name already exist and it now has an active connection session.
How do I decouple the connection from the root module, so it is only ran when needed. Actually cleaner technic will also be accepted.
You don't want to unit test a module, you want to unit test a modules individual components in isolation.
Although you can create a TestModule and simply import your module as you have done above, I would only consider doing that when the module contained a single component (even then I wouldn't as I don't think its very good practice).
The more components you bring into your test:
The more moving parts you need to manage
The more you have to mock
The less portability you have with the unit and its test
The more aspirin you ingest trying resolve self induced headaches that occur every time you modify its parent module
Nests TestingModule enables you to "rig" up an independent module with the bare minimum needed to test your component in isolation. It simplifies your test setups and mock creation/management.
Always try to look at unit testing as a stand alone, independent processes. Limit the scope and dependencies wherever possible to make testing as effective and easy as possible.
Here is an example of the approach I take for unit testing a service where I mock out its dependencies:
// app.service.spec.ts
describe('Testing app.service', () => {
let module: TestingModule;
let service: AppService;
// mock out providers the service depends on
const mockProviders = [
{
provide: ConfigService,
useValue: {
get: jest.fn().mockReturnValue('Mock!'),
},
},
];
beforeAll(async () => {
// build up testing module
module = await Test.createTestingModule({
imports: [],
providers: [...mockProviders, AppService],
})
.compile()
.catch((err) => {
// Helps catch ninja like errors from compilation
console.error(err);
throw err;
});
service = module.get<AppService>(AppService);
});
it('Should return: Hello Mock!', async () => {
const response = service.getHello();
expect(response).toEqual('Hello Mock');
});
});
I try to keep all business logic (wherever possible) in services, leavingcontrollers light and generally reserved for e2e and/or integration testing.
This isn't the only (and maybe not even "the best") approach, but it helps me to keep my tests and services more focused.

Running a global test setup before each test in Jest

I'm trying to have a test setup function executed before each single test in my Jest test suite. I know that I can use beforeEach to accomplish this within a single test file, but I want to do it globally for all my test files without having to explicitly modify each single file.
I looked into the jest configuration file, and noticed a couple of configs that thought could have worked: globalSetup and setupFiles, but they seem to be run only once (at the very beginning of the test run). Like I said, I need it to be run before "each" it block in my test files.
Is this possible?
You could use setupFilesAfterEnv (which replaces setupTestFrameworkScriptFile, deprecated from jest version 24.x) which will run before each test
// package.json
{
// ...
"jest": {
"setupFilesAfterEnv": ["<rootDir>/setupTests.js"]
}
}
And in setupTests.js, you can directly write:
global.beforeEach(() => {
...
});
global.afterEach(() => {
...
});
Just as a follow-up to the answer from #zzz, the more recent documentation on Configuring Jest notes:
Note: setupTestFrameworkScriptFile is deprecated in favor of setupFilesAfterEnv.
So now, your file should look like this:
// package.json
{
// ...
"jest": {
"setupFilesAfterEnv": [
"<rootDir>/setupTests.js"
]
}
}
setupFilesAfterEnv configuration is the way to go, and yes you can use beforeEach in that file and it will run in every test across all the suit.
// jest.config.js
module.exports = {
setupFilesAfterEnv: ['<rootDir>/tests/setupTests.ts']
}
// tests/setupTests.ts
beforeEach(() => {
console.log('before each')
})
afterEach(() => {
console.log('after each')
})
Try it out!

Electron + Jest - ipcRenderer is undefined in unit tests

I am contributing to a project which is built with React (with webpack) running in Electron. When executing unit tests with Jest, it fails with the error TypeError: Cannot read property 'on' of undefined (and works fine when not testing, eg. run with Electron).
The code:
import React, { Component } from 'react';
import { ipcRenderer } from 'electron';
// some more imports
class Setup extends Component {
constructor(props) {
super(props);
this.state = {
// some state
};
ipcRenderer.on('open-file-reply', this.someMethod); // << fails on this line
}
// more class stuff
}
It took me a few days but finally, I found this answer in this great blog post. Quote:
Jest is called from Node and doesn't run test code through Webpack.
Instead, we have to use Jest's mocking functions to replace the import
with a stub file.
Jest has a helper method called moduleNameMapper [object<string, string>] . From jest documentation:
A map from regular expressions to module names that allow to stub out
resources, like images or styles with a single module.
It should be added in your package.json root object like this:
{
"name": "My awesome app",
"jest": {
"moduleNameMapper": {
"electron": "<rootDir>/src/components/tests/mock/electron.js"
}
}
}
and the mock file itself (/src/components/tests/mock/electron.js):
export const ipcRenderer = {
on: jest.fn()
};
This way you can stub other electron modules and methods (like remote which is shown in the blog above).
Another way is creating an electron.js file in __mocks__ in your root folder.
The electron.js should look something like
export const ipcRenderer = {
on: jest.fn(),
};
You can read more at https://jestjs.io/docs/en/manual-mocks#mocking-node-modules

How do you test Vuejs Components when using Vuex

I am writing unit tests for my vuejs components in a larger application. My application state is all in the vuex store, so almost all of my components pull data from vuex.
I can't find a working example for writing unit test for this. I have found
Where Evan You says:
When unit testing a component in isolation, you can inject a mocked store directly into the component using the store option.
But I can't find a good example of how to test these components. I have tried a bunch of ways. Below is the only way I have seen people do it. stackoverflow question and answer
Basically it looks like the
test.vue:
<template>
<div>test<div>
</template>
<script>
<script>
export default {
name: 'test',
computed: {
name(){
return this.$store.state.test;
}
}
}
</script>
test.spec.js
import ...
const store = new Vuex.Store({
state: {
test: 3
}
});
describe('test.vue', () => {
it('should get test from store', () => {
const parent = new Vue({
template: '<div><test ref="test"></test></div>',
components: { test },
store: store
}).$mount();
expect(parent.$refs.test.name).toBe(3);
});
}
Note the "ref" this example doesn't work without it.
Is this really the right way to do this? It seems like it will get messy fast, because it requires adding the props into the template as a string.
Evan's quote seems to imply that the store can be added directly to the child component (ie not the parent like the example).
How do you do that?
The answer is actually really straightforward but not currently documented.
const propsData = { prop: { id: 1} };
const store = new Vuex.Store({state, getters});
const Constructor = Vue.extend(importedComponent);
const component = new Constructor({ propsData, store });
Note the store passed to the constructor. propsData is currently documented, the "store" option isn't.
Also if you are using Laravel, there are weird problems with the webpack versions you may be running.
The
[vuex] must call Vue.use(Vuex),
Error was caused by useing laravel-elixir-webpack-official.
If you did the fix:
npm install webpack#2.1.0-beta.22 --save-dev
for this https://github.com/JeffreyWay/laravel-elixir-webpack-official/issues/17
Your tests that include Vuex seem to break with the
[vuex] must call Vue.use(Vuex)
even when you have Vue.use(Vuex)