简体   繁体   中英

Configuring javascript unit testing and promises assertion

I am new to javascript unit testing, but I believe to be at least a little aware of unit testing in general.

I am trying to learn the 'javascript way' and start writing unit tests for my client code as well.

For this purpose, I installed the following libraries: mocha.js and chai.js

In some surprising turnout, it was all going quite well. I managed to include those things easily and started writing my unit tests.

I am working in ASP Core btw. Though I find the VS2016 the most buggy version of this IDE I have ever used (yet I have been a professional only for 5 years so I could be wrong) with the new MVC 6 changes it offers an easy way to manage your static resources (or client side dependencies) by letting you use a bower package manager.

So, back at the testing. I started with a small list of generic-purposed and simple javascript functions I had laying around. Though a little poorly written, they offered a good exercise.

However, after a while I needed to start mocking. From writing unit tests in C# I have come to appreciate the usefulness of this type of libraries. As those concepts are in no way specific to C# but to unit testing in general, I expected to find a library which will offer this kind of functionality - sinon.js.

So again, everything was going well.

Until, I had to mock a promise.

Again, it turns out that there is a library that should help me with this - sinon-as-promised.js

After quite some effort involving installing it through npm, using tools like browserify to parse it into a single script to be used in the browser and more, I believe I managed to get it running.

At this point I am able to do something like:

let returnsPromise = sinon.stub($, "ajax").resolves('success');

and this would make me a "then-able" object (something that has a then() method and is supposed to behave as a promise). One small problem here, it does not have a .done() and .fail() methods which I have been known to prefer in my coding, but I am guessing that there is an easy way to extend those into the object.

After that, I was even starting to get impressed when I read that chai.js is providing a capable way of managing promise expactations.

And here we are, at my problem.

The thing about chai I am talking about is the "eventually" support. A word that lets me do magic and prevents the need to use .done() in both success and error callbacks of then() just to make sure we are not getting a false positive in the promise.

so, it is supposed to be used like this:

it('testing eventually', function () {
    let returnsPromise = sinon.stub($, "ajax").resolves('success');
    return expect(returnsPromise()).should.eventually.equal('success');        
});

and this should fail if anything other than a success with the wanted result happens.

Now, you can imagine my disappointment when I was unable to get chai-as-promised running.

I couldn't find a 'browser-friendly' version for this library so, again after installing it with npm, browserify-ing it into a 'supposed-to-be-working-in-the-browser-script' I added it to the party.

However, whatever I do, in the example above, I get the following error:

TypeError: Cannot read property 'equal' of undefined
at Context.<anonymous> (js/miscellaneous-tests.js:94:58)

which I interpret as - we don't know what you mean by "eventually". This leads me to believe that the way I added chai-as-promised.js did not work.

After this, I tried a lot of things. Some stupid, some even more so, but whatever I tried it could not let the code to understand "eventually"

those things include:

  • reading the script and getting and adding the dependencies of chai-as-promised to the solution (both as browserify-ed and in the form they come from npm) and in the second scenario do the same for their dependencies again - until I believed no package was missing.
  • I tried adding, removing files in so many different ways I am actually drawing a blank while writing this - but every possible combination I could think of - I tried.
  • I googled "eventually is undefined" and stuff like that so many times I think google is a little pissed with me, yet I couldn't find what I was looking for
  • At some point, I started drawing a large circle with a pentagram in it on the floor, but it took me a while (things like blood from a virgin and candles aren't as easy to find in an office nowadays) and when I came back to the office the next day, the cleaning lady had erased it.

As I am out of ideas, I was hoping some of you guys have tried doing some javascript unit testing in the browser.

Because it involves quite a few resources, explanations and stuff, and this post is already waaaay longer than it should be, I created a github repository in which I have uploaded everything I have done so far.

here is a link to it: js-unit-test-mocking

Btw, if you are not into ASP or anything microsoft related, everything that is needed to run it should be inside the wwwroot.

Thank you for bearing with me. Sorry for the long post.

There are two problems with your code:

  1. You are using expect and should interfaces together ( expect(...).should.eventually.equal...) . You should use one or the other but not both at the same time.

  2. You forget to call chai.use with chai-as-promised 's exported value. I am inferring this from the fact that if I purposely omit this, then I get the exact same error you get, and I don't know of any other way to get that error.

No matter what you want to do, you have to transform chai-as-promised to work in the browser. Since you mention having used browserify , I'm going to use that in the answer. You could use webpack or even write your own script to do the transformation.

Two general strategies follow.

Transform Only chai-as-promised

The strategy here is to keep everything separate and only transform chai-as-promised to work in the browser. Here is an illustrative test file:

const expect = chai.expect;
mocha.setup("bdd");

chai.should();

// Commenting out this line reproduces the exact error message
// reported in the question.
chai.use(chaiAsPromised);

// Incorrect use of should with expect.
it('testing eventually incorrectly', function () {
    return expect(Promise.resolve("success")).should.eventually.equal('success');
});

// Correct use of expect alone.
it('testing eventually with expect', function () {
    return expect(Promise.resolve("success")).eventually.equal('success');
});

// Correct use of should alone.
it('testing eventually with should', function () {
    return Promise.resolve("success").should.eventually.equal('success');
});

chai-as-promised is transformed with this command:

browserify -o chai-as-promised-built.js -s chaiAsPromised node_modules/chai-as-promised/lib/chai-as-promised.js

This essentially takes chai-as-promised.js and wraps it into code so that what it exports normally in the CommonJS dialect becomes a global export in the browser, and that global export is named chaiAsPromised (that's what the -s option does). The output is in chai-as-promise-built.js , which is what you need to load in the browser.

Then you can use this index.html file:

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/xhtml; charset=utf-8"/>
    <link href="./node_modules/mocha/mocha.css" type="text/css" media="screen" rel="stylesheet" />
    <script type="text/javascript" src="./node_modules/mocha/mocha.js"></script>
    <script type="text/javascript" src="./node_modules/chai/chai.js"></script>
    <script type="text/javascript" src="./chai-as-promised-built.js"></script>
    <script type="text/javascript" src="./test.js"></script>
  </head>
  <body>
    <div id="mocha"></div>
    <script>
      mocha.run();
    </script>
  </body>
</html>

Bundle Everything into One File

The strategy here is to use browserify to compile everything into a single file. This is a viable option for some projects. And it also may be preferable for projects that want to have code written using require instead of relying on globals. Here is an illustrative test file:

const chai = require("chai");
const chaiAsPromised = require("chai-as-promised");
const expect = chai.expect;
const mocha = require("mocha");
mocha.mocha.setup("bdd");

chai.should();

// Commenting out this line reproduces the exact error message
// reported in the question.
chai.use(chaiAsPromised);

// Incorrect use of should with expect.
it('testing eventually incorrectly', function () {
    return expect(Promise.resolve("success")).should.eventually.equal('success');
});

// Correct use of expect alone.
it('testing eventually with expect', function () {
    return expect(Promise.resolve("success")).eventually.equal('success');
});

// Correct use of should alone.
it('testing eventually with should', function () {
    return Promise.resolve("success").should.eventually.equal('success');
});

After you install the required module, build it with:

browserify -o built.js test.js

Then you can load it with this index.html file:

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/xhtml; charset=utf-8"/>
    <link href="./node_modules/mocha/mocha.css" type="text/css" media="screen" rel="stylesheet" />
    <script type="text/javascript" src="./built.js"></script>
  </head>
  <body>
    <div id="mocha"></div>
    <script>
      mocha.run();
    </script>
  </body>

You will see that the 1st test will fail because it tries using expect and should together. The two subsequent tests will pass.

If you comment out the line that goes chai.use(chaiAsPromised); you get the exact same error message as what you report in your question. Remember to rebuild with browserify if you do change the code.

I mean, you could make this way more simple by just stubbing ajax with a function that returns a jquery deferred. Something like the following could work:

var dfd = $.Deferred();
var callback = sinon.spy();
sinon.stub($, "ajax", function() {
   return dfd.promise();
})
$.ajax('...').done(callback);
dfd.resolve();
expect(callback).should.have.been.called();

You could also create a helper that does this for you, and an "afterEach" that always replaces ajax with the original function.

The above code is dependent on https://github.com/domenic/sinon-chai . If you don't want to be dependent on sinon-chai then you can just look at the "callCount" of a spy. Like this:

expect(callback.callCount > 0).to.be.ok;

Here is a chaiAsPromised with expect test pattern:

var chai = require("chai");
var expect = chai.expect;
var chaiAsPromised = require("chai-as-promised");
chai.use(chaiAsPromised);

it("promise should return blah", function() {
  var blah = "blah";

  var result = somethingToTest();

  return expect(result).to.eventually.equal(blah);
});

In your test code you're mixing expect and should .

return expect(returnsPromise()).should.eventually.equal('success');

Edit: From the Chai API docs: Chai Assertions for Promises

return doSomethingAsync().should.eventually.equal("foo");

Following the same pattern your code should be:

return returnsPromise().should.eventually.equal('success');

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM