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

  1. System requirements
  2. Using Jasmine 3.99 to detect compatibility problems
  3. Migration path for Ruby and Python users
  4. jasmine package public interface
  5. ES module support
  6. Exit code changes
  7. Changes to how beforeAll and beforeEach failures are handled
  8. Reporter interface changes
  9. Tips for resolving specific deprecation warnings

System requirements

The following previously-supported environments are no longer supported:

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:

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

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

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:

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

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

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();
});