Unit test any JavaScript function with zero dependencies installed.
Maineffect is a testing library that isolates functions from their dependencies at the source level. It parses your code into an AST, strips all imports, and lets you inject only what you need. The function under test runs in a sandbox — no module resolution, no dependency installation, no complex mocking setup.
This means you can test code that depends on databases, APIs, loggers, or any external module without installing any of them.
Jest’s mocking model is powerful but complex. Developers frequently struggle with:
jest.mock() vs jest.fn() vs jest.spyOn() — three overlapping mechanisms with different behaviorsjest.mock() is silently moved above imports, leading to confusing execution order__mocks__ directories, mockImplementation vs mockReturnValue — layers of APIjest.requireActual() — a workaround that reveals the awkwardnessThe result: developers litter tests with console.log statements just to verify their mocks are working. The tool hasn’t made the state of things obvious.
Maineffect’s model is flat. Imports don’t exist. You provide what the function needs. You call it. There’s nothing hidden, no hoisting, no module resolution to reason about. A beginner can understand it in minutes.
import and require statements are removedThis is the same AST parse/transform/generate pipeline that Babel, TypeScript, and Webpack already use in every modern JavaScript project. Maineffect simply adds one transform: removing imports.
And since unit tests should not be concerned with side effects, stripping imports isn’t a compromise — it’s doing exactly what a unit test should do.
npm install maineffectjs
Parse the file (don’t require or import it). Find the function by name. Call it with arguments.
// math.js
import log from 'logger'
const add = (a, b) => a + b
// math.test.js
import { parseFn } from 'maineffectjs'
const math = parseFn(require.resolve('./math'))
describe('add', () => {
it('should return the sum of two numbers', () => {
const result = math.find('add').callWith(51, 82)
expect(result).to.equal(133)
})
})
Notice: add is not exported. The logger module is not installed. The test works anyway.
provideWhen a function uses an external dependency, supply it with provide.
// side-effects.js
import { request } from 'http'
const generateFooService = async () => {
const word = await request('/foo')
return word
}
// side-effects.test.js
import { parseFn } from 'maineffectjs'
const parsed = parseFn(require.resolve('./side-effects'))
it('should return a word using a service', async () => {
const result = await parsed
.find('generateFooService')
.provide('request', () => 'foo')
.callWith()
expect(result).to.equal('foo')
})
No http module needed. No jest.mock(). Just provide the value and call the function.
Real code often has deeply chained calls like logger.stream.foo.bar.info(). With Jest, you’d write:
{
logger: {
stream: {
foo: {
bar: {
info: jest.fn().mockReturnValue(...)
}
}
}
}
}
With Maineffect, describe the chain as a string:
// stubs.js
import logger from 'logger'
const one = () => {
logger.stream.foo.bar.info('adding')
return 1
}
// stubs.test.js
import { parseFn, Stubs } from 'maineffectjs'
const parsed = parseFn(require.resolve('./stubs'))
test('should handle chain of objects', () => {
const stubs = Stubs(jest.fn)
parsed
.find('one')
.stub('logger.stream.foo.bar.info()', stubs.createStub)
.callWith()
expect(stubs.getStubs().info).toBeCalledWith('adding')
})
Keys ending with () become stub functions. Everything else becomes a plain object. Works with any mix of properties and function calls:
.stub('logger().info().debug()', stubs.createStub) // all functions
.stub('logger.info().severe.armageddon()', stubs.createStub) // mixed
Give names to anonymous functions with a comment annotation, then find them like any other function.
// annotations.js
import routes from 'routes'
const get = routes({
method: 'GET',
handler: /*name:vHandler*/() => {
return 1
}
})
// annotations.test.js
import { parseFn } from 'maineffectjs'
const parsed = parseFn(require.resolve('./annotations'), { routes: () => {} })
it('should find annotated fn', async () => {
const result = await parsed.find('vHandler').callWith()
expect(result).toBe(1)
})
Extract components with getFn() and render them with your preferred testing library.
// GreetingWithHooks.js
import React, { useState } from 'react'
const Greeting = ({ greet }) => {
const [name, setName] = useState(greet)
return (
<>
<h1>{`Hello ${name}`}</h1>
<button data-testid="greet" onClick={() => setName(`${name} the great`)} />
</>
)
}
// GreetingWithHooks.test.js
import { parseFn } from 'maineffectjs'
import React, { useState } from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
const parsed = parseFn(require.resolve('./GreetingWithHooks.js'), {
React,
useState,
})
it('should render', () => {
const Greeting = parsed.find('Greeting').getFn()
const { getByTestId } = render(<Greeting greet="FOO" />)
fireEvent.click(getByTestId('greet'))
expect(screen.getByText('Hello FOO the great')).to.be.ok
})
parseFn(filePath, sandbox?, options?)Parse a source file. Returns a chainable CodeFragment object.
Aliases: load, parse
parseFnStr(filePath, sourceString, sandbox?, options?)Parse a source string instead of a file.
| Method | Description |
|---|---|
.find(name) |
Locate a function by name |
.findCallback(name, index) |
Extract a callback from a call expression |
.provide(key, value) |
Inject a dependency by name |
.provide({ key: value, ... }) |
Inject multiple dependencies |
.inject(key, value) |
Alias for provide |
.stub(path, stubCreator) |
Generate nested stubs from a dot-path string |
.callWith(...args) |
Execute the function with arguments |
.apply(thisArg, ...args) |
Execute with a specific this context |
.getFn() |
Return the function without executing it |
.source() |
Return the generated source code |
.print() |
Print the generated source code |
.reset() |
Clear all injected dependencies |
.getProvisions() |
Return all currently injected values |
.getAST() |
Return the raw AST |
.getSandbox() |
Return the sandbox object |
Stubs(stubImplementation)Factory for creating stubs. Pass jest.fn or sinon.stub.
Returns { createStub, getStubs } — use createStub with .stub() and getStubs() to access the generated mocks for assertions.
Maineffect ships two builds:
vm sandboxeval()Because dependencies are stripped at the AST level, there is no module system to hook into. Tests can run in a browser with no bundler, no node_modules, no build pipeline.
npx webpack --config webpack.config.js
npm run test
The core library is ~570 lines. Feel free to send a PR with any feature you think would be useful.
Reach out to me at @buzzarvind on Twitter.
Copyright (c) 2019-2024 Arvind Naidu https://twitter.com/buzzarvind