4.x.x Complete Migration Guide #781

Open
lucasfcosta opened this Issue Sep 3, 2016 · 12 comments

Projects

Done in 4.0

4 participants

@lucasfcosta
Member
lucasfcosta commented Sep 3, 2016 edited

#4.x.x Complete Migration Guide

Hello everyone! I'm writing this to serve as a guide for the 4.x.x version.

To make this as complete as possible I'll include here every single new feature, breaking change and fix and their respective issues and Pull Requests. Bugs introduced and solved after the 3.5.0 released won't be here, since it does not matter for the new version.

I'll also add some examples on how to migrate old code or to use new features when talking about these kinds of changes.

Please let me know if I've forgotten anything 😄


Breaking Changes

  • Instead of allowing the user to write the path of a property, now the deep flag performs a deep equality comparison when used with the .property assertion.
    If you want the old behavior of using the dot or bracket notation to denote the property you want to assert against you can use the new .nested flag. (Related Issues: #745, #743, PRs: #758, #757)

    const obj = {a: 1};
    
    // The `.deep` flag now does deep equality comparisons
    expect({foo: obj}).to.have.deep.property('foo', {a: 1});
    
    // Use the `nested` flag if you want to assert against a nested property using the bracket or dot notation
    expect({foo: obj}).to.have.nested.property('foo.a', 1);
    
    // You can also use both flags combined
    const obj2 = {a: {b: 2}};
    expect({foo: obj2}).to.have.deep.nested.property('foo.a', {b: 2});

    Please notice that the old methods which used the old behavior of the deep flag on the assert interface have been renamed. They all have had the deep word changed by the nested word. If you want to know more about this please take a look at #757.

  • Previously, expect(obj).not.property(name, val) would throw an Error if obj didn't have a property named name. This change causes the assertion to pass instead.
    The assert.propertyNotVal and assert.deepPropertyNotVal assertions were renamed to assert.notPropertyVal and assert.notDeepPropertyVal, respectively. (Related Issues: #16, #743, #758)

  • You can now use the deep flag for the .include assertion in order to perform a deep equality check to see if something is included on the target.
    Previously, .include was using strict equality (===) for non-negated property inclusion, but deep equality for negated property inclusion and array inclusion.
    This change causes the .include assertion to always use strict equality unless the deep flag is set.
    Please take a look at this comment if you want to know more about it. (Related Issues: #743, PRs: #760, #761)

    const obj = {a: 1};
    expect([obj]).to.deep.include({a:1});
    expect({foo: obj}).to.deep.include({foo: {a:1}});
  • Fix unstable behavior of the NaN assertion. Now we use the suggested ES6 implementation.
    The new implementation is now more correct, strict and simple. While the old one threw false positives, the new implementation only checks if something is NaN (or not if the .not flag is used) and nothing else. (Related Issues: #498, #682, #681, PRs: #508)

    // Only `NaN` will be considered to be `NaN` and nothing else
    expect(NaN).to.be.NaN;
    
    // Anything that is not `NaN` cannot be considered as `NaN`
    expect('randomString').not.to.be.NaN;
    expect(true).not.to.be.NaN;
    expect({}).not.to.be.NaN;
    expect(4).not.to.be.NaN;
  • Throw when calling _superon overwriteMethodif the method being overwritten is undefined.
    Currently if the method you are trying to overwrite is not defined and your new method calls _super it will throw an Error.(Related Issues: #467, PRs: #528)
    Before this change, calling _super would simply return this.

    // Considering the method `imaginaryMethod` does not exist, this would throw an error for example:
    chai.use(function (chai, utilities) {
      chai.Assertion.overwriteMethod('imaginaryMethod', function (_super) {
        return function () {
          _super.apply(this, arguments);
        }
      });
    });
    
    // This will throw an error since you are calling `_super` which should be a method (in this case, the overwritten assertion) that does not exist
    expect('anything').to.imaginaryMethod(); 
  • Now showDiff is turned on by default whenever the showDiff flag is anything other than false.
    This issue will mostly affect plugin creators or anyone that made extensions to the core, since this affects the Assertion.assert method. (Related Issues: #574, PRs: #515)

    // Now whenever you call `Assertion.assert` with anything that is not false for the `showDiff` flag it will be true
    // The assertion error that was thrown will have the `showDiff` flag turned on since it was not passed to the `assert` method
    try {
      new chai.Assertion().assert(
          'one' === 'two'
        , 'expected #{this} to equal #{exp}'
        , 'expected #{this} to not equal #{act}'
        , 'one'
        , 'two'
      );
    } catch(e) {
      assert.isTrue(e.showDiff);
    }
    
    // The assertion error that was thrown will have the `showDiff` flag turned off since here we passed `false` explicitly
    try {
      new chai.Assertion().assert(
          'one' === 'two'
        , 'expected #{this} to equal #{exp}'
        , 'expected #{this} to not equal #{act}'
        , 'one'
        , 'two'
        , false
      );
    } catch(e) {
      assert.isFalse(e.showDiff);
    }
  • The Typed Array types are now truncated if they're too long (in this case, if they exceed the truncateThreshold value on the config). (Related Issues: #441, PRs: #576)

    var arr = [];
    for (var i = 1; i <= 1000; i++) {
      arr.push(i);
    }
    
    // The assertion below will truncate the diff shown and the enourmous typed array will be shown as:
    // [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...  ] instead of printing the whole typed array
    chai.expect(new Float32Array(100)).to.equal(1);
  • The assertions: within, above, least, below, most, increase, decrease will throw an error if the assertion's target or arguments are not numbers. (Related Issues: #691, PRs: #692, #796)

    // These will throw errors, for example:
    expect(null).to.be.within(0, 1);
    expect(null).to.be.above(10);
    expect(null).to.be.at.least(20);
    expect(null).to.be.below(20);
    expect(null).to.be.at.most(20);
    expect(null).to.increase.by(20);
    expect(null).to.decrease.by(20);
    
    // This will not:
    expect('string').to.have.a.length.of.at.least(3);
  • Previously, expect(obj).not.ownProperty(name, val) would throw an Error if obj didn't have an own property (non-inherited) named name. This change causes the assertion to pass instead. (Related Issues: #795, #, PRs: #744, #810)*

    expect({ foo: 'baz' }).to.not.have.own.property('quux', 'baz');
  • The .empty assertion will now throw when it is passed non-string primitives and functions (PRs: #763, #812)

    // These will throw TypeErrors:
    expect(Symbol()).to.be.empty;
    expect(function() {}).to.be.empty;
    expect(true).to.be.empty;
    expect(1).to.be.empty
  • Assertion subject (obj) changes when using ownProperty or own.property and thus enables chaining. (Related Issues: #281, PRs: #641)

    expect({val: 20}).to.have.own.property('val').above(10);
  • The utils (second argument passed to the chai.use callback function) no longer exports the getPathValue function. If you want to use that please use the pathval module, which is what chai uses internally now. (Related Issues: #457, #737, PRs: #830)

New Features

  • Throw when non-existent property is read. (Related Issues: #407, #766 PRs: #721, #770)
    This is a potentially breaking change. Your build will fail if you have typos in your property assertions
    Before 4.x.x when using property assertions they would not throw an error if you wrote it incorrectly.
    The example below, for example, would pass:

    expect(true).to.be.ture; // Oops, typo, now Chai will throw an Error

    Since this implementation depends on ES6 Proxies it will only work on platforms that support it.

    This property can be enabled (default) or disabled through the config.useProxy property, for example:

    chai.config.useProxy = false;  // disable use of Proxy
  • Add fix suggestions when accessing a nonexistent property in proxy mode. (Related Issues: #771, PRs: #782)
    When a nonexistent property is accessed in proxy mode, Chai will compute the levenshtein distance to all possible properties in order to suggest the best fix to the user.

    expect(false).to.be.fals; // Error: Invalid Chai property: fals. Did you mean "false"?
    expect('foo').to.be.undefind; // Error: Invalid Chai property: undefind. Did you mean "undefined"?
    
    // If the Levenshtein distance between the word and any Chai property is greater than 4, no fix will be suggested
    expect('foo').to.be.fdsakfdsafsagsadgagsdfasf // error thrown, no fix suggested
  • When non-chainable methods (including overwritten non-chainable methods) are used incorrectly an error will be thrown with a helpful error message. (PRs: #789)

    expect(true).to.equal.true;  // Invalid Chai property: equal.true. See docs for proper usage of "equal".
  • Add a new configuration setting that describes which keys will be ignored when checking for non-existing properties on an assertion before throwing an error.
    Since this implementation depends on ES6 Proxies it will only work on platforms that support it. Also, if you disable config.useProxy, this setting will have no effect. *(Related Issues: #765, PRs: #774)

    chai.config.proxyExcludedKeys.push('nonExistingProp');
    
    expect('my string').to.nonExistingProp; // This won't throw an error now
  • Add script that registers should as a side-effect. (Related Issues: #594, #693 PRs: #604)

    // You can now write:
    import 'chai/should';
    
    // as opposed to:
    import {should} from 'chai';
    should();

    You can also register should via a mocha option: mocha --require chai/should.

  • The change assertion accepts a function as object. (Related Issues: #544, PRs: #607)

    // Now you can also check if the return value of a function changes, for example
    assert.increases(
      someOperation,
      () => getSomething().length
    )
  • You can also assert for a delta using the by assertion alongside the change, increase and decrease assertions. (Related Issues: #339, PRs: #621)

// You can use `.by` to assert the amount you want something to change
var obj = { val: 10 };
var increaseByTwo = function() { obj.val += 2 };
var decreaseByTwo = function() { obj.val -= 2 };
var increaseByThree = function() { obj.val += 3 };

expect(increaseByThree).to.change(obj, 'val').by(3);
expect(increaseByTwo).to.increase(obj, 'val').by(2);
expect(decreaseByTwo).to.decrease(obj, 'val').by(2);

// Please notice that if you want to assert something did change but not by some amount you need to use `.not` **after** the `change` related assertion
// Take a look at the examples below:
expect(increaseByThree).to.change(obj, 'val').but.not.by(5)
expect(increaseByTwo).to.increase(obj, 'val').but.not.by(1)
expect(decreaseByTwo).to.decrease(obj, 'val').but.not.by(1)
  • The .keys assertion can now operate on maps and sets. (Related Issues: #632, PRs: #633, #668)
// The `.keys` assertion now works on `map`s and `set`s natively, like the examples below:
expect(new Map([[{objKey: 'value'}, 'value'], [1, 2]])).to.contain.key({objKey: 'value'});
expect(new Map([[{objKey: 'value'}, 'value'], [1, 2]])).to.contain.any.keys([{objKey: 'value'}, {anotherKey: 'anotherValue'}]);
expect(new Map([['firstKey', 'firstValue'], [1, 2]])).to.contain.all.keys('firstKey', 1);
expect(new Set([['foo', 'bar'], ['example', 1]])).to.have.any.keys('foo');

// You can also use `.deep` when asserting agains `Map`s and `Set`s
expect(new Map([[{objKey: 'value'}, 'value'], [1, 2]])).to.contain.any.deep.keys([{objKey: 'value'}, {anotherKey: 'anotherValue'}]);
expect(new Map([['firstKey', 'firstValue'], [1, 2]])).to.contain.all.deep.keys('firstKey', 1);
expect(new Set([['foo', 'bar'], ['example', 1]])).to.have.any.deep.keys('foo');
  • Add compatibility with strict mode. (Related Issues: #578, PRs: #665)

    // This means you can now run your code with the `--use_strict` flag on Node
    // If want to understand more about this please read the issue related to this change
  • Add does and but as new no-op assertion. (Related Issues: #700, #339 PRs: #621, #701)

    // You can now write assertions forming phrases with these two new words:
    expect(increaseByThree).to.change(obj, 'val').but.not.by(5);
    expect(foobar).to.have.property("baz").which.does.not.have.property("thing");
  • Allow use to be imported using new ES6 module syntax. (Related Issues: #718, PRs: #724)

    // You can now import `use` using the ES6 module syntax, like the example below:
    import sinonChai from "sinon-chai";
    import {expect, use} from "chai";
    
    use(sinonChai);

    You can also use require alongside the new ES6 destructuring feature:

    const sinonChai = require('sinon-chai');
    const {expect, use} = require("chai");
    
    use(sinonChai);
  • Add ordered flag for members assertion. (Related Issues: #717, PRs: #728)

    // You can now use the `ordered` flag to assert the order of elements when using the `members` assertion:
    expect([1, 2, 3]).to.include.ordered.members([1, 2]); // This passes
    expect([1, 2, 3]).to.include.ordered.members([2, 3]); // This will fail! Read the docs to know more.
  • Add .own flag to .property assertion. It does the same thing as .ownProperty and cannot be used alongisde the new .nested flag. (Related Issues: #795, PRs: #810)

    expect({a: 1}).to.have.own.property('a');
    
    // The example below will thrown an Error
    expect({a: {b: 1}}).to.have.own.nested.property('a.b', 1);
  • Add .deep support to .property assertion. (Related Issues: #795, PRs: #810)

    expect({ foo: { bar: 'baz' } }).to.have.deep.own.property('foo', { bar: 'baz' });
    expect({ foo: { bar: { baz: 'quux' } } }).to.have.deep.nested.property('foo.bar', { baz: 'quux' });
  • The .empty assertion will now work with ES6 collections (PRs: #763, #812, #814)
    Please notice that this assertion will throw an error when it is passed a WeakMap or WeakSet.

    expect(new Set()).to.be.empty;
    expect(new Map()).to.be.empty;
    
    // The examples below will throw a TypeError:
    expect(new WeakSet()).to.be.empty;
    expect(new WeakMap()).to.be.empty;

Bug Fixes

  • Fix missing msg argument for change related assertions. (Related Issues: None, PRs: #606)
  • The addMethod function returns a new assertion with flags copied over instead of this. (Related Issues: #562, #684, #723, PRs: #642, #660)
  • Fix stacktraces for overwritten properties and methods. (Related Issues: #659, #661)
  • Fix bug when testing Symbol equality with should syntax. (Related Issues: #669, PRs: #672)
  • Fix bug when asserting some valid ES6 keys. (Related Issues: #674, PRs: #676)
  • Fix bug caused when custom inspect method is used and this method calls stylize. (PRs: #680)
  • Fix ownProperty on objects with no prototype. (Related Issues: #688, PRs: #689)
  • Fix swapped expected and actual results for the .members assertion. (Related Issues: #511, PRs: #702)
  • Fix same.members to properly handle duplicates by treating each one as a unique member. (Related Issues: #590, PRs: #739)
  • Fix deep.have.same.members() that was not printing a diff. (PRs: #509)
  • Diff will be shown for assert's equal and notEqual methods. (Related Issues: #790, PRs: #794)
  • The overwriteMethod, overwriteProperty, addChainableMethod, overwriteChainableMethod functions will return new assertion with flags copied over instead of this. (Related Issues: #562, #642, #791, PRs: #799)
@lucasfcosta
Member
lucasfcosta commented Sep 3, 2016 edited

I accidentally sent this issue while editing it.
This is a work in progress.


Current Status: This issue has just been updated (Sept. 6, 2016 - 10:13PM GMT-3) with every commit on the master branch. I'm now going to start adding the changes from the 4.x.x branch.

Since we've got a holiday in Brazil this Wednesday, I may be finishing this before the weekend.
I'm keeping this file on vIM, I'll make sure to edit this issue again with the whole content when I'm done covering every commit.

@lucasfcosta lucasfcosta changed the title from 4.x.x Complete Migration Guide to [WIP] 4.x.x Complete Migration Guide Sep 3, 2016
@keithamus
Member

Don't forget to check out the draft 4.0.0 release notes @lucasfcosta - I've tried to keep them relatively up to date, although lately I've missed quite a few PRs.

@lucasfcosta
Member
lucasfcosta commented Sep 5, 2016 edited

@keithamus thanks for the link! I'll take a look at it.
I'll try to keep this issue as complete as possible so we will be able to have a detailed guide if users want more information about any of the topics listed and then we can just copy and paste some info from here and add it to the release notes (with some formatting fixes of course).

Currently I'm using gitk and seeing each single commit since the 3.5.0 tag both on the master and 4.x.x branches.

@keithamus keithamus modified the milestone: 4.0 Sep 6, 2016
@lucasfcosta lucasfcosta changed the title from [WIP] 4.x.x Complete Migration Guide to 4.x.x Complete Migration Guide Oct 2, 2016
@lucasfcosta
Member
lucasfcosta commented Oct 2, 2016 edited

This is done! 🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉
Now the migration guide has every change on the master and 4.x.x branch documented until today (Oct, 2nd, 2016).

Special thanks to @vieiralucas for helping me finish this.

PARTY HARD

@keithamus
Member

Thanks for this amazing work @lucasfcosta

@meeber
Contributor
meeber commented Oct 4, 2016

Will try to review after work!

@meeber
Contributor
meeber commented Oct 4, 2016

@lucasfcosta Thank you for the enormous effort in tracking all of this down and organizing it! Also thank you for the dancing pikachu.

A quick review yielded the following:

  • I think breaking changes should be listed before new features.
  • In the list of new features, I think the proxy stuff should be listed first as I believe it addresses Chai's most complained about issue.
  • Add script that registers should as a side-effect: Given mocha's popularity, it might be worth noting that this change allows you to register should via a mocha option: mocha --require chai/should.
  • The change assertion accepts a function as objec: The "t" is missing in object. Also this needs to be explained a bit more with clearer examples. It also needs to mention that change, increase, and decrease all have this new functionality. Finally, I think we need to update the actual jsdoc for change because, unlike increase and decrease, it doesn't mention this new feature.
  • The you can also assert for a delta using the by assertion alongside change, increase and decrease assertion: Grammar issue going on here.
  • The .keys assertion can now operate on maps and set: Nitpick, but I'd say either don't pluralize "maps" or do pluralize "set". I prefer the first option.
  • Allow use to be imported using new ES6 module syntax: It might be worth noting and exemplifying that this can also be done with require thanks to object destructuring assignment: const {expect, use} = require("chai");
  • This change causes the .include assertion to always use strict equality: Suggest rephrasing this to something like "This change causes the .include assertion to always use strict equality unless the deep flag is set".
  • Suggest changing "While the old one throwed up false positives" to "While the old one threw false positives".
  • Fix stacktraces for overwritten properties and method: Method should be plural.
  • Fix bug when testing Symbol equality with should synta: What happened to that poor "x"??
  • Fix bug when asserting some valid ES6 key: Key should be plural.
  • Fix ownProperty on objects with no prototyp: What happened to that poor "e"??
@lucasfcosta
Member
lucasfcosta commented Oct 4, 2016 edited

@meeber thanks for the review!
Actually lots of last characters ended up being replaced by a . due to my badly applied regex skills for auto-replace, so lots of those issues come from that.
I'll fix those ASAP!

EDIT: Fixed! Thanks for the help Mr. @meeber, your feedback is always great and constructive!

@lucasfcosta lucasfcosta added the docs label Oct 8, 2016
@meeber
Contributor
meeber commented Oct 19, 2016

Doing a last review here, noticed a couple things:

  • "Please notice that the old the old methods" (typo)
  • "The assert.propertyNotVal and assert.deepPropertyNotVal assertions were renamed to assert.notPropertyVal and assert.deepPropertyNotVal" (the final assert.deepPropertyNotVal should be assert.notDeepPropertyVal)
  • "expect([1, 2, 3]).to.include.ordered.members([2, 3]); // This will! Read the docs to know more." (missing "fail" in comment)
@lucasfcosta
Member
lucasfcosta commented Oct 20, 2016 edited

@meeber done!
Thank you for the review, it's always good to have your eyes on the things I write.
If anyone finds anything else that should be changed, let me know 😄

Also, we could also a link on the README for the ones interested into the migration guide, I think it would be useful, what do you guys think?

@inancgumus
Contributor
inancgumus commented Nov 18, 2016 edited

Hi @lucasfcosta You may want to update for this PR too: #868 and #872

@lucasfcosta
Member
lucasfcosta commented Nov 18, 2016 edited

Hi @deeperx, thanks for your input.
This is the migration guide for 4.0.0 only, which has already been released under the canary tag.
I think that changes made after that version should be within release logs, not in this guide. However, if the rest of the team finds it suitable we may add it here.

@astorije astorije referenced this issue in astorije/chai-immutable Dec 18, 2016
Open

Update for Chai v4.0 #69

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment