Last active
July 8, 2022 14:01
-
-
Save ryyppy/e60376024aa9e4fe2962f3ab13e87bf0 to your computer and use it in GitHub Desktop.
Jest: Module Mocking vs. Simple Dependency Injection
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ############################### | |
// runPackager.js | |
// ############################### | |
// In this example, the IO function is tightly coupled with the implementation | |
import { execFileSync } from 'child_process'; | |
type RunPackagerOptions = { | |
projectRoot: string, // CWD the react-native binary is being run from | |
targetDir: string, // Target directory absolute or relative to projectRoot (e.g. 'rna/') | |
version: string, // Version number to for the build (semver) | |
platform: 'ios' | 'android', | |
entryFile: string, // File without any extension (e.g. 'index') | |
}; | |
function runPackager(options: RunPackagerOptions): void { | |
// stuff happens here | |
// args = [...] | |
// opts = {...} | |
// Here, the IO effect takes place... this is our IO dependency | |
// we need to know is being called (in our tests, we need to know that | |
// this dependency is being used... we cannot read it from our FN signature) | |
execFileSync('react-native', args, opts); | |
} | |
// ############################### | |
// __tests__/runPackager-test.js | |
// ############################### | |
import runPackager from '../runPackager'; | |
// In Jest, I need to mock child_process.execFileSync, | |
// jest will analyze the 'child_process' structure and replace | |
// every function with a mocked representation | |
js.mock('child_process'); | |
describe('...', () => { | |
it('should use mocked execFileSync', () => { | |
// Get the jest mock function from the mocked module | |
const { execFileSync } = require('child_process'); | |
// Do the stuff where the same execFileSync is being used | |
runPackager({}); | |
// Check the output | |
expect(execFileSync.mock.call[0]).to.equal([]); | |
// Jest will reset the modules after every `it` case, so that's nice | |
}) | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ############################### | |
// runPackager.js | |
// ############################### | |
// Here, we give the possibility to switch out the execFileSync implementation | |
// via a parameter option and default to the original implementation. | |
import child_process from 'child_process'; | |
type RunPackagerOptions = { | |
// ...some attributes... | |
// Now, here we explicitly mention the IO dependency in our parameters | |
execFileSync?: typeof child_process.execFileSync, // IO dependency | |
}; | |
function runPackager(options: RunPackagerOptions) { | |
// If execFileSync is not set, use the nodejs implementation | |
const { execFileSync = child_process.execFileSync } = options; | |
// stuff happens here | |
// args = [...] | |
// opts = {...} | |
execFileSync('react-native', args, opts); | |
} | |
// ############################### | |
// __tests__/runPackager-test.js | |
// ############################### | |
import runPackager from '../runPackager'; | |
// Now, jest.mock() is unnecessary, since we know we | |
// can just hand in our execFileSync via parameter | |
describe('...', () => { | |
it('should use our execFileSync mock', () => { | |
// It's a nobrainer,... just create the function | |
// isolated from every other test run | |
const execFileSync = jest.fn(); | |
runPackager({ execFileSync }); | |
// Check the result... that's it. No clean up / reset needed | |
expect(execFileSync.mock.call[0]).toEqual([]); | |
}); | |
}); |
Thanks for the helpful snippets
By the way, at hard-dependency.js:38 I think you mean jest.mock
instead of js.mock
.
Thank you so much for this; was having trouble wrapping my head around this, especially comparing the two approaches (tightly coupled, vs dependency injection) and testing each one.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
After playing around with
jest.mock()
I realized I can reduce this example by removing the verbosebeforeEach
stuff... in everyit
, the mocked modules will be reset... which is very convenient and isolates the tests well!So there is only one advantage for Dependency Injection left: The dependencies are explicitly mentioned in the function parameters and we don't have to dive into the implementation details of the function to test.