Upgrading to Jasmine 4.0
Overview
tl;dr: Update to 3.99 and fix any deprecation warnings before updating to 4.0.
We think that Jasmine 4.0 will be an easy upgrade for most users. However, it does contain a number of breaking changes and some people will need to make changes to their code in order to upgrade. Following the steps in this guide will give you the greatest chance of a smooth upgrade experience.
Breaking changes in Jasmine 4.0 include changes to how Jasmine is configured,
changes to custom matchers and asymmetric equality testers, better detection
of common errors, and changes to the system requirements. You can find a
complete list of breaking changes in the release notes for
jasmine-core
and jasmine
.
Contents
- System requirements
- Using Jasmine 3.99 to detect compatibility problems
- Migration path for Ruby and Python users
jasmine
package public interface- ES module support
- Exit code changes
- Changes to how beforeAll and beforeEach failures are handled
- Reporter interface changes
- Tips for resolving specific deprecation warnings
System requirements
The following previously-supported environments are no longer supported:
- Node <12.17
- Internet Explorer
- Firefox 68 and 78
- Safari 8-13
- PhantomJS
- Python
- Ruby
- Bower
Although Jasmine 4.0 may still work in some of those environments, we no longer test against them and won’t try to maintain compatibility with them in future 4.x releases. Odd-numbered Node versions (13.x, 15.x, 17.x) are unsupported and may or may not work. In particular, some 13.x versions have incomplete support for ES/CommonJS module interop and can’t run Jasmine 4.0.
Using Jasmine 3.99 to detect compatibility problems
Jasmine 3.99 issues deprecation warnings for most code that uses APIs that are
removed or changed in incompatible ways in 4.0. We recommend upgrading to 3.99
and fixing all deprecation warnings before upgrading to 4.0. You should upgrade
the Jasmine packages that you depend on directly, as usual. If you upgrade the
jasmine
NPM package, the jasmine
Rubygem, or the jasmine
Python package to
3.99, you’ll also get version 3.99 of jasmine-core.
If you’re using jasmine-browser-runner, just add jasmine-core
3.99 to your
dependencies.
Migration path for Ruby and Python users
3.99 is the final version of Jasmine for Ruby and Python. We recommend that most
users migrate to the jasmine-browser-runner
NPM package, which is the direct replacement for the jasmine
Ruby gem and
Python package. It runs your specs in browsers, including headless Chrome and
Saucelabs. It works with Rails applications including those that use Webpacker,
which was never supported by the jasmine
gem.
If jasmine-browser-runner
doesn’t meet your needs, one of these might:
- The jasmine NPM package to run specs in Node.js.
- The standalone distribution to run specs in browsers with no additional tools.
- The jasmine-core NPM package if all
you need is the Jasmine assets. This is the direct equivalent of the
jasmine-core
Ruby gem and Python package.
jasmine
package public interface
Unlike the jasmine-core
pacakge, the jasmine
package did not have a
documented public interface until version 3.8. Beginning with 4.0, anything
that’s not part part of the documented public interface will be considered a
private API and subject to change at any time. If you use the jasmine
package
programmatically (i.e. you have code that does require('jasmine'
) or
import('jasmine')
, please check your code against the
API docs.
ES module support
Beginning with version 4, the jasmine
package defaults to loading specs and
other files using dynamic import()
. This allows ES modules with .js
extension to be used without any configuration. Version 4.0.1 fixes the loading
of files with nonstandard extensions like .jsx
or .coffee
, so be sure to
install at least that version if you have such files.
Exit code changes
Prior to version 4, the jasmine
command exited with a status of 1 in almost
all scenarios other than complete success. Version 4 uses different exit codes
for different types of failures. You don’t need to change anything unless your
build or CI system specifically checks for an exit code of 1 (this is very
unusual). Anything that treats 0 as success and everything else as failure will
still work.
Changes to how beforeAll
and beforeEach
failures are handled
Jasmine 4 handles beforeAll
and beforeEach
failures differently. The change
may cause problems with certain unusual setup and teardown patterns. As long as
you tear down resources in the same suite where they were set up, you don’t
need to make any changes.
When a beforeAll
function fails for any other reason other than a failed
expectation, Jasmine 4 will skip the entire suite except for any afterAll
functions defined in the same suite as the failed beforeAll
. Similarly, a
beforeEach
failure other than a failed expectation will cause any subsequent
beforeEach
functions, the spec in question, and any afterEach
functions
defined in nested suites to be skipped.
// Unsafe. Test pollution can result because the afterEach won't always run.
describe('Outer suite', function() {
beforeEach(function() {
setSomeGlobalState();
possiblyFail();
});
describe('inner suite', function() {
it('does something', function() { /*...*/ });
// This afterEach function should be moved up to the outer suite.
afterEach(function() {
cleanUpTheGlobalState();
});
});
});
Reporter interface changes
Jasmine 4.0 adds a debugLogs
field to the
object that’s passed to a reporter’s specDone method.
It will be defined if the spec called
jasmine.debugLog
and also failed. Most reporters should display it if it’s present.
Jasmine 4.0 is more likely than previous versions to report errors in the
jasmineDone
event. Failing to display those errors is a common bug in custom
reporters. You can check yours by creating an afterAll
function at the top
level (i.e. not in a describe
) that throws an exception and making sure that
your reporter displays it.
Tips for resolving specific deprecation warnings
Deprecations related to custom equality testers in matchers
- “The matcher factory for [name] accepts custom equality testers, but this parameter will no longer be passed in a future release”
- “Passing custom equality testers to MatchersUtil#contains is deprecated.”
- “Passing custom equality testers to MatchersUtil#equals is deprecated.”
- “Diff builder should be passed as the third argument to MatchersUtil#equals, not the fourth.”
Prior to 3.6, custom matchers
that wanted to support custom equality testers
had to accept an array of custom equality testers as the matcher factory’s
second argument and pass it to methods like MatchersUtil#equals
and
MatchersUtil#contains
. Since 3.6, the MatchersUtil
instance passed to the
matcher factory is preconfigured with the current set of custom equality
testers, and matchers do not need to provide them. Beginning with Jasmine 4.0,
custom equality testers are neither passed to the matcher factory nor accepted
as parameters to MatchersUtil
methods. To update your custom matchers to the
new style, simply remove the extra parameter from the matcher factory and stop
passing it to MatchersUtil
methods.
Before:
jasmine.addMatchers({
toContain42: function(matchersUtil, customEqualityTesters) {
return {
compare: function(actual, expected) {
return {
pass: matchersUtil.contains(actual, 42, customEqualityTesters)
};
}
};
}
});
After:
jasmine.addMatchers({
toContain42: function(matchersUtil) {
return {
compare: function(actual, expected) {
return {
pass: matchersUtil.contains(actual, 42)
};
}
};
}
});
Note to library authors
The updated form above is only compatible with Jasmine 3.6 and newer. If you
want to maintain compatibility with older versions of Jasmine, you can avoid the
deprecation warnings by declaring the matcher factory to take a single argument
and then using the arguments
object or
rest parameter syntax
to access the array of custom equality testers. Then check for a deprecated
property on the array, which will be present in Jasmine 3.6 and later, before
passing it along:
jasmine.addMatchers({
toContain42: function(matchersUtil, ...extraArgs) {
const customEqualityTesters =
extraArgs[0] && !extraArgs[0].deprecated ? extraArgs[0] : undefined;
return {
compare: function(actual, expected) {
return {
pass: matchersUtil.contains(actual, 42, customEqualityTesters)
};
}
};
}
});
"The second argument to asymmetricMatch is now a MatchersUtil. Using it as an array of custom equality testers is deprecated and will stop working in a future release."
Prior to 3.6, custom asymmetric equality testers
that wanted to support custom equality testers
had to accept an array of custom equality testers as second argument to
asymmetricMatch
and pass it to methods like MatchersUtil#equals
and
MatchersUtil#contains
. From 3.6, to 3.99, the second argument is both an
array of custom equality testers and a MatchersUtil
instance that’s
preconfigured with them. In 4.0 and later it’s just a properly configured
MatchersUtil
. To resolve the deprecation warning, use the provided
MatchersUtil
directly:
Before:
function somethingContaining42() {
return {
asymmetricMatch: function(other, customEqualityTesters) {
return jasmine.matchersUtil.contains(other, 42, customEqualityTesters);
}
};
}
After:
function somethingContaining42() {
return {
asymmetricMatch: function(other, matchersUtil) {
return matchersUtil.contains(other, 42);
}
};
}
Note to library authors
The updated form above is only compatible with Jasmine 3.6 and newer. If you
want to maintain compatibility with older versions of Jasmine, you can avoid the
deprecation warnings by checking whether the second argument is a MatchersUtil
(e.g. by checking for an equals
method) and falling back to the old form above
if it isn’t a MatchersUtil
.
Deprecations related to matcher utilities that are no longer available globally
- “jasmine.matchersUtil is deprecated and will be removed in a future release.
Use the instance passed to the matcher factory or the asymmetric equality
tester’s
asymmetricMatch
method instead.” - “jasmine.pp is deprecated and will be removed in a future release. Use the
pp method of the matchersUtil passed to the matcher factory or the asymmetric
equality tester’s
asymmetricMatch
method instead.”
Until Jasmine 4.0 there was a static MatchersUtil
instance available as
jasmine.matchersUtil
and a static pretty printer available as jasmine.pp
.
Because both of those now carry configuration that’s specific to the current
spec, it no longer makes sense to have static instances. Instead of using
jasmine.matchersUtil
, access the current MatchersUtil
in one of the
following ways:
- The first parameter to a matcher factory
- The second parameter to an asymmetric equality tester’s
asymmetricMatch
method
Instead of using jasmine.pp
, access a matchersUtil
in either of the above
ways and then use its pp method.
Also, if an object has a jasmineToString
method, pp
will be passed as the
first parameter.
Note to library authors
matchersUtil
is provided to matcher factories in all versions since 2.0, and
to asymmetric equality testers in all versions since 2.6. matchersUtil#pp
was
introduced in 3.6, which is also the first version that passed a pretty-printer
to jasmineToString
. If you need to pretty-print in a way that’s compatible with
Jasmine versions older than 3.6, you can check for a pp
method on the
matchersUtil
instance or a parameter passed to jasmineToString
and then
fall back to jasmine.pp
if it isn’t there.
Deprecations related to mixing two forms of async
- “An asynchronous before/it/after function was defined with the async keyword but also took a done callback. This is not supported and will stop working in the future. Either remove the done callback (recommended) or remove the async keyword.”
- “An asynchronous before/it/after function took a done callback but also returned a promise. This is not supported and will stop working in the future. Either remove the done callback (recommended) or change the function to not return a promise.”
Jasmine never supported functions that combine multiple forms of async, and they never had a consistent or well-defined behavior. Jasmine 4.0 will issue a spec failure whenever it encounters such a function. The FAQ discusses the reason for this change and how to update your specs.
Deprecations due to calling done
multiple times
- “An asynchronous function called its ‘done’ callback more than once. This is a bug in the spec, beforeAll, beforeEach, afterAll, or afterEach function in question. This will be treated as an error in a future version.”
- “A top-level beforeAll or afterAll function called its ‘done’ callback more than once. This is a bug in the beforeAll or afterAll function in question. This will be treated as an error in a future version.”
Jasmine historically tolerated multiple done
calls, but the bugs that this
masked proved to be a common source of confusion. Jasmine 4 will report an
error whenever an asynchronous function calls done
more than once.
The FAQ discusses the reason for this change and how to update your specs.
Deprecations due to reentrant calls to jasmine.clock().tick()
Prior to 4.0, calling jasmine.clock().tick()
from inside a setTimeout
or
setInterval
handler could cause the current time as exposed by Date
to
travel backwards. Beginning with 4.0, the current time will not decrease even
in the case of reentrant calls to tick()
. This might affect the behavior of
specs that both call tick()
from inside a timer handler and also care about
the current time.
You can ignore this warning if the affected specs don’t care about the current
time, or if you plan to check them after upgrading to 4.0. However, we recommend
modifying the affected specs to not call tick()
from inside a timer handler.
Before:
it('makes a reentrant call to tick()', function() {
const foo = jasmine.createSpy('foo');
const bar = jasmine.createSpy('bar');
setTimeout(function() {
foo();
jasmine.clock().tick(9);
}, 1);
setTimeout(function() {
bar();
}, 10);
jasmine.clock().tick(1);
expect(foo).toHaveBeenCalled();
expect(bar).toHaveBeenCalled();
});
After:
it('does not make a reentrant call to tick()', function() {
const foo = jasmine.createSpy('foo');
const bar = jasmine.createSpy('bar');
setTimeout(function() {
foo();
}, 1);
setTimeout(function() {
bar();
}, 10);
jasmine.clock().tick(1);
expect(foo).toHaveBeenCalled();
jasmine.clock().tick(9);
expect(bar).toHaveBeenCalled();
});