-
Notifications
You must be signed in to change notification settings - Fork 409
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Chore: Add jest unit test for salesforcedx-utils package #4470
Conversation
config/jest.base.config.js
Outdated
@@ -2,7 +2,7 @@ | |||
module.exports = { | |||
preset: 'ts-jest', | |||
testEnvironment: 'node', | |||
testMatch: [ "**/unit/**/?(*.)+(spec|test).[t]s?(x)" ], | |||
testMatch: [ "**/(unit|jest)/**/?(*.)+(spec|test).[t]s?(x)" ], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unit tests will now be valid under a unit or jest directory.
The intention is the jest directory is the new location for jest true unit tests.
{ | ||
testMatch: [ "**/unit/**/?(*.)+(spec|test).[t]s?(x)" ], | ||
} | ||
baseConfig |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
clean up unnecessary override.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cool..yeah it's a js file so can be ignored. I can add a ignore comment I think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just an FYI this was only showing u in the utils projects which using eslint vs tslint. So I only updated that config file
export const NO_STDOUT_ERROR = 'No stdout found for childProcess'; | ||
export const NO_STDERR_ERROR = 'No stderr found for childProcess'; | ||
export const CANCELATION_INTERVAL = 1000; | ||
export const KILL_CODE = 'SIGKILL'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
exported constants so they can be verified in unit tests.
@@ -83,79 +74,3 @@ export class CliCommandExecutor { | |||
); | |||
} | |||
} | |||
|
|||
export class CompositeCliCommandExecutor { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
put class in it's own file for cleaner organization and import.
import { CommandExecution } from '../types/commandExecution'; | ||
import { Command } from './command'; | ||
|
||
export class CompositeCliCommandExecution implements CommandExecution { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nothing new here just moved to a separate file.
import { Command } from './command'; | ||
import { CompositeCliCommandExecution } from './compositeCliCommandExecution'; | ||
|
||
export class CompositeCliCommandExecutor { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same story: 1 class exported to its own file.
@@ -10,7 +10,8 @@ import { CommandOutput } from './commandOutput'; | |||
import { SfdxCommandBuilder } from './sfdxCommandBuilder'; | |||
/** | |||
* @deprecated | |||
* NOTE: This code is deprecated in favor of using ConfigUtil.ts |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why I deleted this NOTE honestly. Probably need to visit if this command is necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be good to put this back? Or add to the same line as deprecated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok..yeah I'll put it back. This is only used in the debuggers currently that can't access configUtils in the vscode-utils package FYI.
@@ -1,7 +1,8 @@ | |||
import { CommandBuilder } from './commandBuilder'; | |||
|
|||
export const SFDX_COMMAND = 'sfdx'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should probably put this in constants.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, good idea!
@@ -27,59 +19,3 @@ export class Localization implements LocalizationProvider { | |||
return this.delegate.localize(label, ...args); | |||
} | |||
} | |||
|
|||
export type MessageBundle = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same story as above. Organize by putting exports in their own files.
@@ -0,0 +1,5 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
our first snapshot. These are generated by jest the first time a shouldMatchSnapshot
type call is made. Then after the value found here is compare on the matchSnapshot call in future tests. If it doesn't match the assert will fail. If the value has change you can updated it with a flag passed to the test (included in the failure output).
Thing to take away is snapshot should be treated as code and reviewed closely on change
} from '../../../src/cli/cliCommandExecution'; | ||
import { Observable } from 'rxjs/Observable'; | ||
import * as kill from 'tree-kill'; | ||
jest.mock('tree-kill'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This mocks the module in memory in jest. So when it's used in the code under test the underlying functions are just mocks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this will become a standard practice for us to use across jest tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
correct. If you need to mock and entire module for a test this is the easiest way to do it.
import * as kill from 'tree-kill'; | ||
jest.mock('tree-kill'); | ||
|
||
const treeKillMocked = jest.mocked(kill); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is how you correctly type the mocked function for use in the test while keeping typescript happy.
} as any); | ||
intervalSpy = jest.spyOn(Observable, 'interval').mockReturnValue({ | ||
subscribe: subscribeSpy | ||
} as any); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is testing a class that uses rxjs. It leads to some pretty strange tests.
}); | ||
|
||
afterEach(() => { | ||
jest.resetAllMocks(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've worked on projects previously where we set this in the global after for all tests. I'm not sure if that's the right move or not but we could put that in place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be cool. Is there ever a situation where one would want to not have this happen after a test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No I wouldn't think so. It wouldn't be goodto have one test depending on setup from another so I think we could just reset all Mocks by default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep sounds good to me!
testCommand, | ||
testChildProcess | ||
); | ||
expect(cliCommandExecution).toBeInstanceOf(CliCommandExecution); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note that when using jest it automatically injects jest and it's components like expect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK so that means they don't have to be explicitly imported?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
correct
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
solid
'data' | ||
]); | ||
|
||
expect(subscribeSpy).toHaveBeenCalledTimes(2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a ton of good options for jest expects: https://jestjs.io/docs/expect
testChildProcess, | ||
testCancelationToken | ||
); | ||
}).toThrowError(NO_PID_ERROR); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the syntax for verifying that a function call throws an error. It's a little weird that you have to wrap the call in an arrow function (I almost always forget).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be good to add in tests.md
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better yet, add an instruction to create a snippet... or maybe we could ship some 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 I'll follow on this PR with an update to tests.md
expect(subscribeSpy).toHaveBeenCalledTimes(3); | ||
const timoutHandler = subscribeSpy.mock.calls[2][0]; | ||
const timeoutPromise = timoutHandler(); | ||
expect(treeKillMocked).toHaveBeenCalledTimes(1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note how we can use the treeKillMocked here to verify that an dependent module was called.
killCallback(); | ||
} else { | ||
fail('Should have had a kill callback function.'); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The treekill function is odd in that it takes a callback as a parameter. To test the callback code I had to call it here.
import { CliCommandExecution } from '../../../src/cli/cliCommandExecution'; | ||
import { GlobalCliEnvironment } from '../../../src/cli/globalCliEnvironment'; | ||
jest.mock('cross-spawn'); | ||
jest.mock('../../../src/cli/cliCommandExecution'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note this is a mock for a third party module vs a local module.
|
||
beforeEach(() => { | ||
// Add a global value to the GCE option for processing during creation. | ||
GlobalCliEnvironment.environmentVariables.set(globalKey, globalValue); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is an exported Map so I decided to just set a value verse mocking the Map functionality.
fakeCommand, | ||
fakeChildProcess, | ||
undefined | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note we're not calling the underlying executor here but just verifying that it should have been called.
let getCmdResultSpy: jest.SpyInstance; | ||
beforeEach(() => { | ||
executeSpy = jest | ||
.spyOn(CliCommandExecutor.prototype, 'execute') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jest.spyOn
=~ sandbox.stub
.mockReturnValue(fakeExecution as any); | ||
getCmdResultSpy = jest | ||
.spyOn(CommandOutput.prototype, 'getCmdResult') | ||
.mockResolvedValue(fakeResult); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: mockResolved
value returns a promise wrapped results. mockReturnValue
is without promises.
getCmdResultSpy.mockResolvedValue(partialJson); | ||
const forceConfigGet = new ForceConfigGet(); | ||
// Unexpected token error is thrown b/c json can not be parsed. | ||
expect(forceConfigGet.getConfig(fakePath)).rejects.toThrowError(/Unexpected token/); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the syntax for checking if an error is thrown by making a call.
expect(commandBuilderInst).toBeInstanceOf(CommandBuilder); | ||
const testCommand = commandBuilderInst.build(); | ||
expect(testCommand).toBeInstanceOf(Command); | ||
expect(testCommand.command).toBe(testCommandStr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: I sort of figured out as I went that .toBe
and .toEqual
are pretty much the same but toBe won't work sometimes which led me to search for the diff where I found https://dev.to/thejaredwilcurt/why-you-should-never-use-tobe-in-jest-48ca#:~:text=toEqual%20and%20the%20.,equality%20fails%20on%20non%2Dprimatives.
So bottom line we should probably just default to toEqual
I'll update these tests to get rid of toBe
}, | ||
], | ||
} | ||
`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
much more interesting snapshots here. Note here it is useful b/c I don't have to check all the properties individually or in a constructed object in the call like headers, data, etc.
}, | ||
"timeout": 20000, | ||
"type": "POST", | ||
"url": "https://where.is/the/instance/https://this.is.a.test", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll verify why this looks weird.
const result = await requestServiceInst.execute(testCommand); | ||
expect(result).toBe(fakeResponse.responseText); | ||
expect(sendRequestMock).toHaveBeenCalled(); | ||
expect(sendRequestMock).toMatchSnapshot(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should probably update this to snapshot on the args and not the entire mock
.withLogName('testLogName'); | ||
|
||
const command = new Command(commandBuilderInst); | ||
expect(command.toString()).toMatchSnapshot(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
example snapshot expect
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly questions and spelling; thanks for getting all of this ramped up for us!
@@ -7,6 +7,10 @@ | |||
|
|||
import { Command } from './command'; | |||
|
|||
export const JSON_FLAG = '--json'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love this centralized place for these. Our libraries could do with the same treatment, if we were investing there.
@@ -10,7 +10,8 @@ import { CommandOutput } from './commandOutput'; | |||
import { SfdxCommandBuilder } from './sfdxCommandBuilder'; | |||
/** | |||
* @deprecated | |||
* NOTE: This code is deprecated in favor of using ConfigUtil.ts |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be good to put this back? Or add to the same line as deprecated
@@ -1,7 +1,8 @@ | |||
import { CommandBuilder } from './commandBuilder'; | |||
|
|||
export const SFDX_COMMAND = 'sfdx'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, good idea!
if (args.length >= 1) { | ||
const expectedNumArgs = possibleLabel.split('%s').length - 1; | ||
if (args.length !== expectedNumArgs) { | ||
// just log it, we might want to hide some in some languges on purpose |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: typo on languages
} | ||
} | ||
|
||
return format(possibleLabel, ...labelArgs); // TODO: verify this works |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
..does it? :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it does :)
expect(command.logName).toBeUndefined(); | ||
}); | ||
|
||
it('Should be abled to pass a fully built builder.', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: typo on abled
expect(command.toString()).toEqual(testDescription); | ||
}); | ||
|
||
it('Should construct string if no descirption provided.', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: typo on description
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, are we building a command too? Test name is a little unclear
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah this is verifying that the toString method works as expected.
let commandOutput: CommandOutput; | ||
let result: Promise<string>; | ||
|
||
// It's a pain to unit test Obserables. Open to exploring other options. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Obserables -> Observables
@@ -0,0 +1,102 @@ | |||
import { CommandOutput } from '../../../src'; | |||
|
|||
describe('CommandOuput Unit Tests.', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: CommandOuput -> CommandOutput
expect(label).toEqual(expected); | ||
}); | ||
|
||
it('Should only partially populate if too few args are passed.', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't it also log an error, or was that elsewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't make the rules...just tested that it works they way it was coded :)
@@ -32,12 +34,12 @@ export class RequestService { | |||
private _proxyAuthorization!: string; | |||
private _connectionTimeoutMs: number = DEFAULT_CONNECTION_TIMEOUT_MS; | |||
|
|||
public getEnvVars(): any { | |||
public getEnvVars(): NodeJS.ProcessEnv { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍🏽
it('Should have special handling for the json flag.', () => { | ||
const testCommand = commandBuilderInst.withArg(JSON_FLAG); | ||
expect(testCommand.args).toEqual( | ||
expect.arrayContaining([JSON_FLAG, LOG_LEVEL_FLAG, FATAL]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const labelKey = 'notAKey'; | ||
const label = message.localize(labelKey); | ||
const expected = `${MISSING_LABEL_MSG} ${labelKey}`; | ||
expect(label).toEqual(expected); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that is correct...I'll see what I can do
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great to me, Gordon. Just the one question about the console.warn thing. Thanks for bringing jest into the fold!
What does this PR do?
Create the first set of jest unit tests that are using purely jest for unit testing.
What issues does this PR fix or reference?
@W-11309496@
Functionality Before
Functionality After