The Jest Roblox API is similar to the API used by JavaScript Jest.
Jest Roblox doesn't inject any global variables. Every jest functionality needs to be imported from JestGlobals
. For example:
local JestGlobals = require(Packages.Dev.JestGlobals)
local describe = JestGlobals.describe
local expect = JestGlobals.expect
local test = JestGlobals.test
The above code is equivalent to JavaScript's:
import {describe, expect, test} from '@jest/globals'.
There are two variations of expect
in jest-roblox
:
This is strictly typed and is used with built in Jest matchers. For example:
-- ... JestGlobals, test defined prior to this
local expect = JestGlobals.expect
test('some test', function()
local foo;
expect(1).toBe(1) -- toBe is a built-in Jest matcher
expect(foo).toBeDefined() -- toBeDefined is a built-in Jest matcher
end)
This is loosely typed and is meant to be used with custom matchers. For example:
-- ... JestGlobals, test defined prior to this
local expectExtended = JestGlobals.expectExtended
-- extending expect to support our custom matcher `toBeEmptyString`
expectExtended.extend({
toBeEmptyString = function(self, received: unknown, expected: unknown, options: OptionsReceived?)
-- dummy implementation
local pass = received == ""
local message = if pass
then ("%s is an empty string"):format(received)
else ("%s is an NOT empty string"):format(received)
return { actual = received, message = message, pass = pass }
end,
})
test("some test", function()
local foo
expectExtended(foo).toBeEmptyString() -- toBeEmptyString is a custom Jest matcher
end)
In this case, since we are using a custom matcher toBeEmptyString()
, if we had used the normal expect
from JestGlobals.expect
, then roblox-analyze
would have thrown errors because of type issues.
Note In converted code (ie. code converted by the JS to Lua tool), we often import
expectExtended
and assign it to a variableexpect
in order to reduce the amount of deviations in the file. We also mark this import as a deviation. Here's an example of such a situation:
-- ... JestGlobals, test defined prior to this
-- ROBLOX deviation START: importing expectExtended to avoid analyze errors for additional matchers
local expect = JestGlobals.expectExtended
-- ROBLOX deviation END
test('some test', function()
local foo;
-- in this case, we get to preserve the originally converted code which uses `expect` by simply assigning `expectExtended` to `expect`
expect(foo).toBeEmptyString()
end)
test('another test', function()
local bar = "fizz";
-- in this case, we get to preserve the originally converted code which uses `expect` by simply assigning `expectExtended` to `expect`
expect(bar).toBeEmptyString()
end)
Using both expect
and expectExtended
In a manually written file where there are both tests that use both built-in and custom matchers, it may not be possible to simply re-assign expectExtended
to a variable named expect
since this would mean losing type safety for built-in matchers.
In such a case, we opt for having two imports and using either expect
or expectExtended
depending on whether we are using built-in matchers or custom matchers.
Example:
-- ... JestGlobals, test defined prior to this
local expect = JestGlobals.expect
local expectExtended = JestGlobals.expectExtended
test('test using built-in matcher - uses expect', function()
local foo;
expect(foo).toBe(nil)
end)
test('test using custom matcher - uses expectExtended', function()
local bar = "fizz";
expectExtended(bar).toBeEmptyString()
end)
This way, we can preserve the type-safety that comes with expect
for built-in matchers and take advantage of the flexibility and extensibility that expectExtended
provides when using custom matchers.
TL;DR - When to use expect
vs expectExtended
- When writing your own tests, if you are using built-in Jest matchers, then you should use
expect
. - If you are using custom matchers, you should use
expectExtended
Note:
.not
is renamed to.never
to avoid collision with Lua reserved key word.expect
andexpectExtended
can also be swapped out with each other in the below examples. The differences between the two has been covered above.
-
expect(value)
-
expect.extend(matchers)
-
expect.anything()
-
expect.any(constructor)
-
expect.nothing()
-
expect.never
-
expect.arrayContaining
-
expect.arrayNotContaining
- deviation: equivalent toexpect.not.arrayContaining
-
expect.objectContaining
-
expect.objectNotContaining
- deviation: equivalent toexpect.not.objectContaining
-
expect.stringContaining
-
expect.stringNotContaining
- deviation: equivalent toexpect.not.stringContaining
-
expect.stringMatching
-
expect.stringNotMatching
- deviation: equivalent toexpect.not.stringMatching
-
expect.addSnapshotSerializer
-
expect.getState
-
expect.setState
-
.never
- deviation: used in place of.not
-
.toBe(value)
-
.toBeCloseTo(number, numDigits?)
-
.toBeDefined()
-
.toBeFalsy()
-
.toBeGreaterThan(number)
-
.toBeGreaterThanOrEqual(number)
-
.toBeInstanceOf
-
.toBeLessThan(number)
-
.toBeLessThanOrEqual(number)
-
.toBeNan()
- Alias for.toBeNaN
-
.toBeNaN()
-
.toBeNil()
-
.toBeNull()
- Alias for.toBeNil
-
.toBeTruthy()
-
.toBeUndefined()
- Alias for.toBeNil
-
.toContain(item)
-
.toContainEqual(item)
-
.toEqual(value)
-
.toHaveLength(number)
-
.toHaveProperty(keyPath, value?)
-
.toMatch(regexp | string)
-
.toMatchInstance(instance)
- deviation: Roblox only (matches on RobloxInstance
) -
.toMatchObject(object)
-
.toMatchSnapshot(propertyMatchers?, hint?)
-
.toStrictEqual(value)
-
.toBeCalled()
-
.toBeCalledTimes(number)
-
.toBeCalledWith(nthCall, arg1, arg2, ...)
-
.toHaveBeenCalled()
-
.toHaveBeenCalledTimes(number)
-
.toHaveBeenCalledWith(arg1, arg2, ...)
-
.toHaveBeenLastCalledWith(arg1, arg2, ...)
-
.toHaveBeenNthCalledWithtoHaveBeenNthCalledWith(nthCall, arg1, arg2, ...)
-
.toHaveLastReturnedWith(value)
-
.toHaveNthReturnedWith(nthCall, value)
-
.toHaveReturned()
-
.toHaveReturnedTimes(number)
-
.toHaveReturnedWith(value)
-
.toReturn()
-
.toReturnTimes(number)
-
.toReturnWith(value)
-
.toThrow()
-
.toThrowError(error)
-
.toThrowErrorMatchingSnapshot(hint?)
expect.closeTo(number, numDigits?)
expect.hasAssertions()
.resolves
.rejects
.toMatchInlineSnapshot(propertyMatchers?, inlineSnapshot)
.toThrowErrorMatchingInlineSnapshot(inlineSnapshot)
expect.assertions(number)
Tagged templates are not available in Lua. As an alternative, a headings string must be used with a list of arrays containing the same amount of items as headings. Eg:
each({
{ 0, 1, 1 },
{ 1, 1, 2 }
})("returns $expected when given $a and $b",
function(ref)
local a: number, b: number, expected = ref.a, ref.b, ref.expected
jestExpect(a + b).toBe(expected)
end
)
Concurrent methods are NOT supported ATM:
test.concurrent(name, fn, timeout)
test.concurrent.each(table)(name, fn, timeout)
test.concurrent.only.each(table)(name, fn)
test.concurrent.skip.each(table)(name, fn)
Adjusted note:
Note: If a promise is returned from test, Jest will wait for the promise to resolve before letting the test complete. Jest will also wait if you provide a 2nd argument to the test function, usually called done. This could be handy when you want to test callbacks. See how to test async code here.
jest.resetModules()
jest.isolateModules(fn)
jest.mock(moduleName, factory, options)
jest.unmock(moduleName)
jest.requireActual(moduleName)
jest.disableAutomock()
jest.enableAutomock()
jest.createMockFromModule(moduleName)
jest.doMock(moduleName, factory, options)
jest.dontMock(moduleName)
jest.setMock(moduleName, moduleExports)
jest.requireMock(moduleName)
jest.useFakeTimers()
jest.useRealTimers()
jest.runAllTimers()
jest.runOnlyPendingTimers()
jest.clearAllTimers()
jest.advanceTimersByTime(msToRun: number)
jest.advanceTimersToNextTimer(steps: number?)
jest.getTimerCount()
jest.setSystemTime(now: (number | DateTime)?)
jest.getRealSystemTime()
jest.runAllTicks()
jest.runAllImmediates()
mockFn.getMockName()
mockFn.mock.calls
mockFn.mock.results
mockFn.mock.instances
mockFn.mock.lastCall
mockFn.mockClear()
mockFn.mockReset()
mockFn.mockRestore()
mockFn.mockImplementation(fn)
mockFn.mockImplementationOnce(fn)
mockFn.mockName(value)
mockFn.mockReturnThis()
mockFn.mockReturnValue(value)
mockFn.mockReturnValueOnce(value)
- Note this can be easily addedjest.isMockFunction(fn)
- Notejest.spyOn(object, methodName)
spyOn
is a feature that we need and have created a workarounds for nowjest.spyOn(object, methodName, accessType?)
jest.mocked<T>(item: T, deep = false)
mockFn.mockResolvedValue(value)
mockFn.mockResolvedValueOnce(value)
mockFn.mockRejectedValue(value)
mockFn.mockRejectedValueOnce(value)
-jest.MockedFunction
jest.MockedClass
jest.setTimeout(timeout)
jest.retryTimes()
Configuration only available via project's jest.config.lua
file. This file needs to be placed in the src
directory (or directly in Workspace
(see WorkspaceStatic
directory mapping in jest.project.json
))
-- Sync object
local config = {
verbose = true,
}
return config
-- Or async function
return Promise.resolve():andThen(function()
return {
verbose = true,
}
end)
These correspond to JS Jest options
deviation: type means that the option's type deviates from the original upstream types
-
clearMocks
[boolean] -
displayName
[string, object] -
projects
[array] deviation: type -
restoreMocks
[boolean] -
rootDir
[Instance] deviation: type -
roots
[array] deviation: type -
setupFiles
[array] deviation: type -
setupFilesAfterEnv
[array] deviation: type -
slowTestThreshold
[number] -
snapshotSerializers
[array] deviation: type -
testFailureExitCode
[number] -
testMatch
[array] -
testPathIgnorePatterns
[array] -
testRegex
[string | array] -
testTimeout
[number] -
verbose
[boolean]
globalSetup
[string] Not supported YETglobalTeardown
[string] Not supported YET
ref: https://jestjs.io/docs/27.x/configuration#options
automock
[boolean]bail
[number | boolean]cacheDirectory
[string]collectCoverage
[boolean]collectCoverageFrom
[array]coverageDirectory
[string]coveragePathIgnorePatterns
[array]coverageProvider
[string]coverageReporters
[array<string | [string, options]>]coverageThreshold
[object]dependencyExtractor
[string]errorOnDeprecated
[boolean]extensionsToTreatAsEsm
[array]extraGlobals
[array]forceCoverageMatch
[array]globals
[object]haste
[object]injectGlobals
[boolean]maxConcurrency
[number]maxWorkers
[number | string]moduleDirectories
[array]moduleFileExtensions
[array]moduleNameMapper
[object<string, string | array>]modulePathIgnorePatterns
[array]modulePaths
[array]notify
[boolean]notifyMode
[string]preset
[string]prettierPath
[string]reporters
[array<moduleName | [moduleName, options]>]resetMocks
[boolean]resetModules
[boolean]resolver
[string]runner
[string]snapshotFormat
[object]snapshotResolver
[string]testEnvironment
[string]testEnvironmentOptions
[Object]testResultsProcessor
[string]testRunner
[string]testSequencer
[string]testURL
[string]timers
[string] NOTE: not sure about thistransform
[object<string, pathToTransformer | [pathToTransformer, object]>]transformIgnorePatterns
[array]unmockedModulePathPatterns
[array]watchPathIgnorePatterns
[array]watchPlugins
[array<string | [string, Object]>]watchman
[boolean]
TBD
Should be the same taking into account not supported matchers
Tests can return a Promise and Jest Roblox will wait for the promise to resolve.
Note: It is also worth noting that tests can ONLY return either a
Promise
ornil
.
Jest Roblox supports done
callback as a second param to the test function
eg.
test("the data is peanut butter", function(_ctx, done)
function callback(error_, data)
if error_
done(error_);
return
end
xpcall(function()
expect(data).toBe('peanut butter');
done();
end, function(err)
done(err)
end)
end
fetchData(callback)
end);
NOT SUPPORTED YET
TODO